Skip to content

Commit 66a2125

Browse files
authored
Dynamic Node Labels (#386)
* WIP * Update * fix existing tests * update examples * wip * update docs * update tgbn label semantics * add some tests * more tests * more tests * permissions * coverage * spelling * Address comments
1 parent 3eda848 commit 66a2125

27 files changed

Lines changed: 1354 additions & 229 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,19 +135,19 @@ class RecurrentGCN(torch.nn.Module):
135135
136136
# Initialize our model and optimizer
137137
encoder = RecurrentGCN(node_dim=static_node_x.shape[1], embed_dim=128)
138-
decoder = NodePredictor(in_dim=128, out_dim=train_dg.node_x_dim)
138+
decoder = NodePredictor(in_dim=128, out_dim=train_dg.node_y_dim)
139139
opt = torch.optim.Adam(set(encoder.parameters()) | set(decoder.parameters()), lr=0.001)
140140
141141
# Training loop
142142
h_0 = None
143143
for batch in train_loader:
144144
opt.zero_grad()
145-
y_true = batch.node_x
145+
y_true = batch.node_y
146146
if y_true is None:
147147
continue
148148
149149
z, h_0 = encoder(batch, static_node_x, h_0)
150-
z_node = z[batch.node_x_nids]
150+
z_node = z[batch.node_y_nids]
151151
y_pred = decoder(z_node)
152152
153153
loss = F.cross_entropy(y_pred, y_true)

docs/quickstart.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,19 @@ class RecurrentGCN(torch.nn.Module):
4040

4141
# Initialize our model and optimizer
4242
encoder = RecurrentGCN(node_dim=static_node_x.shape[1], embed_dim=128)
43-
decoder = NodePredictor(in_dim=128, out_dim=train_dg.node_x_dim)
43+
decoder = NodePredictor(in_dim=128, out_dim=train_dg.node_y_dim)
4444
opt = torch.optim.Adam(set(encoder.parameters()) | set(decoder.parameters()), lr=0.001)
4545

4646
# Training loop
4747
h_0 = None
4848
for batch in train_loader:
4949
opt.zero_grad()
50-
y_true = batch.node_x
50+
y_true = batch.node_y
5151
if y_true is None:
5252
continue
5353

5454
z, h_0 = encoder(batch, static_node_x, h_0)
55-
z_node = z[batch.node_x_nids]
55+
z_node = z[batch.node_y_nids]
5656
y_pred = decoder(z_node)
5757

5858
loss = F.cross_entropy(y_pred, y_true)

docs/tutorials/dgraph_tutorial.md

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,27 @@ class DGData:
5454
edge_mask (Tensor): Mask of edge events within `time`.
5555
edge_index (Tensor): Edge connections [num_edge_events, 2].
5656
edge_x (Tensor | None): Optional edge features [num_edge_events, D_edge].
57-
node_mask (Tensor | None): Mask of node events within `time`.
58-
node_x_nids (Tensor | None): Node IDs corresponding to node events [num_node_events].
59-
node_x (Tensor | None): Node features over time [num_node_events, D_node_dynamic].
57+
node_x_mask (Tensor | None): Mask of dynamic node features within `time`.
58+
node_x_nids (Tensor | None): Node IDs corresponding to dynamic node features [num_node_events].
59+
node_x (Tensor | None): Dynamic Node features over time [num_node_events, D_node_dynamic].
60+
node_y_mask (Tensor | None): Mask of node labels within `time`.
61+
node_y_nids (Tensor | None): Node IDs corresponding to node labels [num_node_labels].
62+
node_y (Tensor | None): Node labels over time [num_node_labels, D_node_dynamic].
6063
static_node_x (Tensor | None): Node features invariant over time [num_nodes, D_node_static].
64+
edge_type (Tensor | None) : Type of relation of each edge event in edge_index [num_edge_events].
65+
node_type (Tensor | None) : Type of each node [num_nodes].
6166
6267
Raises:
6368
InvalidNodeIDError: If an edge or node ID match `PADDED_NODE_ID`.
69+
InvalidNodeIDError: If node labels exists with node IDs outside the graph's node ID range.
6470
ValueError: If any data attributes have non-well defined tensor shapes.
6571
EmptyGraphError: If attempting to initialize an empty graph.
6672
6773
Notes:
6874
- Timestamps must be non-negative and sorted; DGData will sort automatically if necessary.
6975
- Cloning creates a deep copy of tensors to prevent in-place modifications.
76+
- Edge type is only applicable for Heterogeneous & Knowledge graph.
77+
- Node type is only applicable for Knowledge graph.
7078
"""
7179
```
7280

@@ -114,10 +122,14 @@ Please consult our documentation for full description of our API. The table belo
114122
| `edge_src_col` | Column name in edge file for src nodes | `str` | Yes | Cannot have ids matching `tgm.constants.PADDED_NODE_ID` |
115123
| `edge_dst_col` | Column name in edge file for dst nodes | `str` | Yes | Cannot have ids matching `tgm.constants.PADDED_NODE_ID` |
116124
| `edge_time_col` | Column name in edge file for edge times | `str` | Yes | Time must be non-negative |
117-
| `node_file_path` | Path to CSV file containing dynamic node data | `str \| pathlib.Path` | No | `node_df` is using `from_pandas` |
118-
| `node_x_nids_col` | Column name in node file for node event node ids | `str` | No, unless `node_file_path` is specified | Cannot have ids matching `tgm.constants.PADDED_NODE_ID` |
119-
| `node_x_time_col` | Column name in node file for node event node times | `str` | No, unless `node_file_path` is specified | Time must be non-negative |
125+
| `node_x_file_path` | Path to CSV file containing dynamic node data | `str \| pathlib.Path` | No | `node_x_df` is using `from_pandas` |
126+
| `node_x_nids_col` | Column name in node file for node event node ids | `str` | No, unless `node_x_file_path` is specified | Cannot have ids matching `tgm.constants.PADDED_NODE_ID` |
127+
| `node_x_time_col` | Column name in node file for node event node times | `str` | No, unless `node_x_file_path` is specified | Time must be non-negative |
120128
| `node_x_col` | Column name in node file for dynamic node features | `str` | No | |
129+
| `node_y_file_path` | Path to CSV file containing dynamic node labels | `str \| pathlib.Path` | No | `node_y_df` is using `from_pandas` |
130+
| `node_y_nids_col` | Column name in node file for node label node ids | `str` | No, unless `node_y_file_path` is specified | Cannot have ids matching `tgm.constants.PADDED_NODE_ID` |
131+
| `node_y_time_col` | Column name in node file for node label node times | `str` | No, unless `node_y_file_path` is specified | Time must be non-negative |
132+
| `node_y_col` | Column name in node file for dynamic node labels | `str` | No | |
121133
| `static_node_x_file_path` | Path to CSV file containing static node features | `str \| pathlib.Path` | No | `static_node_x_df` if using `from_pandas` |
122134
| `static_node_x_col` | Column name in static node feats file for static node features | `str` | No, unless `static_node_x_file_path` is specified | |
123135
| `time_delta` | Time granularity of the graph data | `TimeDeltaDG \| str` | Yes | Default to *event_ordered* granularity `'r'` |
@@ -131,16 +143,18 @@ A few key things to know:
131143
- We expect an `edge_file_path` which is a csv file with `edge_src_col`, `edge_dst_col`, `edge_time_col` as a minimum.
132144
- Your edge csv file may also contain `edge_x_col` which are the edge features on your data
133145
- dynamic node data (optional)
134-
- If included, we expect a `node_file_path` which is a csv file with `node_x_nids_col`, `node_x_time_col` as a minimum. These are your dynamic node events.
146+
- If included, we expect a `node_x_file_path` which is a csv file with `node_x_nids_col`, `node_x_time_col` as a minimum. These are your dynamic node events.
147+
- If included, we expect a `node_y_file_path` which is a csv file with `node_y_nids_col`, `node_y_time_col` as a minimum. These are your dynamic node labels.
135148
- Your dynamic node data csv file may also include `node_x_col`, which are the dynamic node features in your data.
149+
- Your dynamic node labels csv file may also include `node_y_col`, which are the dynamic node labels in your data.
136150
- static node data (optional)
137151
- If included, we expect a `static_node_x_file_path` which is a csv file with `static_node_x_col`, the static node features for your dataset.
138152

139153
Internally, we perform various checks on the tensors shapes, node ranges, and timestamps values. If your data is well structured, everything should work. If you get an error message that is not intuitive, please let us know.
140154

141155
#### From Pandas
142156

143-
The API largely the same as above, except that we expected `edge_df`, `node_df`, and `static_node_x_df` dataframes for the edge, dynamic node, and static node data respectively, instead of csv files.
157+
The API largely the same as above, except that we expected `edge_df`, `node_x_df`, and `static_node_x_df` dataframes for the edge, dynamic node, and static node data respectively, instead of csv files.
144158

145159
```python
146160
import pandas as pd
@@ -171,7 +185,7 @@ dg = DGData.from_pandas(
171185
edge_dst_col='dst',
172186
edge_time_col='t',
173187
edge_x_col='edge_feat',
174-
node_df=dynamic_node_df,
188+
node_x_df=dynamic_node_df,
175189
node_x_nids_col='node',
176190
node_x_time_col='t',
177191
node_x_col='dynamic_node_feat',
@@ -404,6 +418,9 @@ class DGBatch:
404418
node_x (Tensor | None, optional): Dynamic node features for nodes in the batch. Tensor of shape `(T x V x d_node_dynamic)`.
405419
node_x_time (Tensor | None, optional): Timestamps corresponding to dynamic node features.
406420
node_x_nids (Tensor | None, optional): Node IDs corresponding to dynamic node features.
421+
node_y (Tensor | None, optional): Dynamic node labels for nodes in the batch. Tensor of shape `(T x V x d_node_labels)`.
422+
node_y_time (Tensor | None, optional): Timestamps corresponding to dynamic node labels.
423+
node_y_nids (Tensor | None, optional): Node IDs corresponding to dynamic node labels.
407424
"""
408425
```
409426

docs/tutorials/hook_tutorial.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class MyNegativeHook(StatelessHook):
4848
requires = set()
4949

5050
def __call__(self, dg: DGraph, batch: DGBatch) -> DGBatch:
51-
batch.my_neg = torch.randint(10, 100, (len(batch.dst),))
51+
batch.my_neg = torch.randint(10, 100, (len(batch.edge_dst),))
5252
batch.my_neg_time = batch.edge_time.clone()
5353
return batch
5454
```

examples/nodeproppred/dygformer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def train(
176176
for batch in tqdm(loader):
177177
opt.zero_grad()
178178

179-
y_true = batch.node_x
179+
y_true = batch.node_y
180180
if len(batch.edge_src) > 0:
181181
z = encoder(batch, static_node_x) # [num_nodes, embed_dim]
182182

@@ -210,12 +210,12 @@ def eval(
210210
static_node_x = loader.dgraph.static_node_x
211211

212212
for batch in tqdm(loader):
213-
y_true = batch.node_x
213+
y_true = batch.node_y
214214

215215
if batch.edge_src.shape[0] > 0:
216216
z = encoder(batch, static_node_x)
217217
if y_true is not None:
218-
z_node = z[batch.node_x_nids]
218+
z_node = z[batch.node_y_nids]
219219
y_pred = decoder(z_node)
220220
input_dict = {
221221
'y_true': y_true,
@@ -263,7 +263,7 @@ def eval(
263263
val_loader = DGDataLoader(val_dg, batch_size=args.bsize, hook_manager=hm)
264264
test_loader = DGDataLoader(test_dg, batch_size=args.bsize, hook_manager=hm)
265265

266-
num_classes = train_dg.node_x_dim
266+
num_classes = train_dg.node_y_dim
267267

268268
encoder = DyGFormer_NodePrediction(
269269
num_nodes=full_data.num_nodes,

examples/nodeproppred/gclstm.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,12 @@ def train(
8181

8282
for batch in tqdm(loader):
8383
opt.zero_grad()
84-
y_true = batch.node_x
84+
y_true = batch.node_y
8585
if y_true is None:
8686
continue
8787

8888
z, h_0, c_0 = encoder(batch, static_node_x, h_0, c_0)
89-
z_node = z[batch.node_x_nids]
89+
z_node = z[batch.node_y_nids]
9090
y_pred = decoder(z_node)
9191

9292
loss = F.cross_entropy(y_pred, y_true)
@@ -116,12 +116,12 @@ def eval(
116116
static_node_x = loader.dgraph.static_node_x
117117

118118
for batch in tqdm(loader):
119-
y_true = batch.node_x
119+
y_true = batch.node_y
120120
if y_true is None:
121121
continue
122122

123123
z, h_0, c_0 = encoder(batch, static_node_x, h_0, c_0)
124-
z_node = z[batch.node_x_nids]
124+
z_node = z[batch.node_y_nids]
125125
y_pred = decoder(z_node)
126126

127127
input_dict = {
@@ -152,7 +152,7 @@ def eval(
152152
val_loader = DGDataLoader(val_dg, batch_unit=args.snapshot_time_gran)
153153
test_loader = DGDataLoader(test_dg, batch_unit=args.snapshot_time_gran)
154154

155-
num_classes = train_dg.node_x_dim
155+
num_classes = train_dg.node_y_dim
156156

157157
encoder = RecurrentGCN(
158158
node_dim=train_dg.static_node_x_dim, embed_dim=args.embed_dim

examples/nodeproppred/gcn.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ def train(
100100

101101
for batch in tqdm(loader):
102102
opt.zero_grad()
103-
y_true = batch.node_x
103+
y_true = batch.node_y
104104
if y_true is None:
105105
continue
106106

107107
z = encoder(batch, static_node_x)
108-
z_node = z[batch.node_x_nids]
108+
z_node = z[batch.node_y_nids]
109109
y_pred = decoder(z_node)
110110

111111
loss = F.cross_entropy(y_pred, y_true)
@@ -131,12 +131,12 @@ def eval(
131131
static_node_x = loader.dgraph.static_node_x
132132

133133
for batch in tqdm(loader):
134-
y_true = batch.node_x
134+
y_true = batch.node_y
135135
if y_true is None:
136136
continue
137137

138138
z = encoder(batch, static_node_x)
139-
z_node = z[batch.node_x_nids]
139+
z_node = z[batch.node_y_nids]
140140
y_pred = decoder(z_node)
141141

142142
input_dict = {
@@ -167,7 +167,7 @@ def eval(
167167
val_loader = DGDataLoader(val_dg, batch_unit=args.snapshot_time_gran)
168168
test_loader = DGDataLoader(test_dg, batch_unit=args.snapshot_time_gran)
169169

170-
num_classes = train_dg.node_x_dim
170+
num_classes = train_dg.node_y_dim
171171

172172
encoder = GCNEncoder(
173173
in_channels=train_dg.static_node_x_dim,

examples/nodeproppred/persistant_forecast.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ def eval(
5252
perf_list = []
5353

5454
for batch in tqdm(loader):
55-
y_true = batch.node_x
55+
y_true = batch.node_y
5656
if y_true is None:
5757
continue
5858

5959
y_pred = torch.zeros_like(y_true)
60-
for i, node_id in enumerate(batch.node_x_nids.tolist()):
60+
for i, node_id in enumerate(batch.node_y_nids.tolist()):
6161
y_pred[i] = model(node_id)
6262
model.update(node_id, y_true[i])
6363

@@ -83,7 +83,7 @@ def eval(
8383
val_loader = DGDataLoader(val_dg, batch_unit=args.snapshot_time_gran)
8484
test_loader = DGDataLoader(test_dg, batch_unit=args.snapshot_time_gran)
8585

86-
num_classes = train_dg.node_x_dim
86+
num_classes = train_dg.node_y_dim
8787
model = PersistantForecaster(num_classes=num_classes)
8888

8989
eval(train_loader, model, evaluator)

examples/nodeproppred/tgat.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def train(
144144

145145
for batch in tqdm(loader):
146146
opt.zero_grad()
147-
y_labels = batch.node_x
147+
y_labels = batch.node_y
148148
if y_labels is not None:
149149
z = encoder(batch, static_node_x)
150150
y_pred = decoder(z)
@@ -172,7 +172,7 @@ def eval(
172172
static_node_x = loader.dgraph.static_node_x
173173

174174
for batch in tqdm(loader):
175-
y_labels = batch.node_x
175+
y_labels = batch.node_y
176176
if y_labels is not None:
177177
z = encoder(batch, static_node_x)
178178
y_pred = decoder(z)
@@ -198,20 +198,20 @@ def eval(
198198
val_dg = DGraph(val_data, device=args.device)
199199
test_dg = DGraph(test_data, device=args.device)
200200

201-
num_classes = train_dg.node_x_dim
201+
num_classes = train_dg.node_y_dim
202202

203203
if args.sampling == 'uniform':
204204
nbr_hook = NeighborSamplerHook(
205205
num_nbrs=args.n_nbrs,
206-
seed_nodes_keys=['node_x_nids'],
207-
seed_times_keys=['node_x_time'],
206+
seed_nodes_keys=['node_y_nids'],
207+
seed_times_keys=['node_y_time'],
208208
)
209209
elif args.sampling == 'recency':
210210
nbr_hook = RecencyNeighborHook(
211211
num_nbrs=args.n_nbrs,
212212
num_nodes=full_data.num_nodes, # Assuming node ids at test set > train/val set
213-
seed_nodes_keys=['node_x_nids'],
214-
seed_times_keys=['node_x_time'],
213+
seed_nodes_keys=['node_y_nids'],
214+
seed_times_keys=['node_y_time'],
215215
)
216216
else:
217217
raise ValueError(f'Unknown sampling type: {args.sampling}')
@@ -225,8 +225,6 @@ def eval(
225225
val_loader = DGDataLoader(val_dg, args.bsize, hook_manager=hm)
226226
test_loader = DGDataLoader(test_dg, args.bsize, hook_manager=hm)
227227

228-
num_classes = train_dg.node_x_dim
229-
230228
encoder = TGAT(
231229
node_dim=train_dg.static_node_x_dim,
232230
edge_dim=train_dg.edge_x_dim,

examples/nodeproppred/tgcn.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,12 @@ def train(
8080

8181
for batch in tqdm(loader):
8282
opt.zero_grad()
83-
y_true = batch.node_x
83+
y_true = batch.node_y
8484
if y_true is None:
8585
continue
8686

8787
z, h_0 = encoder(batch, static_node_x)
88-
z_node = z[batch.node_x_nids]
88+
z_node = z[batch.node_y_nids]
8989
y_pred = decoder(z_node)
9090

9191
loss = F.cross_entropy(y_pred, y_true)
@@ -114,12 +114,12 @@ def eval(
114114
static_node_x = loader.dgraph.static_node_x
115115

116116
for batch in tqdm(loader):
117-
y_true = batch.node_x
117+
y_true = batch.node_y
118118
if y_true is None:
119119
continue
120120

121121
z, h_0 = encoder(batch, static_node_x, h_0)
122-
z_node = z[batch.node_x_nids]
122+
z_node = z[batch.node_y_nids]
123123
y_pred = decoder(z_node)
124124

125125
input_dict = {
@@ -150,7 +150,7 @@ def eval(
150150
val_loader = DGDataLoader(val_dg, batch_unit=args.snapshot_time_gran)
151151
test_loader = DGDataLoader(test_dg, batch_unit=args.snapshot_time_gran)
152152

153-
num_classes = train_dg.node_x_dim
153+
num_classes = train_dg.node_y_dim
154154

155155
encoder = RecurrentGCN(
156156
node_dim=train_dg.static_node_x_dim, embed_dim=args.embed_dim

0 commit comments

Comments
 (0)