File size: 27,361 Bytes
76f20a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
import torch
import torch.nn as nn
from functools import partial, cache
from argparse import Namespace
from typing import List, Tuple, Dict, Union, Optional
from itertools import chain
import random
from typing import Literal

from transformers import T5Tokenizer

class Graph():
    """
    A graph class.
    :param g: A list of tuples, where each tuple is a triple (head, r, tail).
    """
    def __init__(
            self, 
            g: List[Tuple[str,str,str]] = []
        ):
        self.g = g
        self.concepts = self.get_concepts()  # list of all concepts in the graph
        self.relations = self.get_relations()  # list of all relations in the graph
        self.relations_multiple = self.get_relations_multiple()  # list of all relations in the graph, including duplicate relations

    @property
    def g(self) -> List[Tuple[str,str,str]]:
        return self._g

    @g.setter
    def g(self, g: List[Tuple[str,str,str]]):
        self._g = g

    def num_triplets(self) -> int:
        """
        Get the number of triplets in the graph.
        """
        return len(self.g)

    def get_concepts(self) -> List[str]:
        """
        Get the concepts in the graph.
        """
        concepts = list(set([triplet[i] for triplet in self.g for i in [0, 2]]))
        concepts.sort()  # not necessary but makes debugging easier
        return concepts    
    
    def get_relations(self) -> List[str]:
        """
        Get the relations in the graph.
        """
        relations = list(set(self.get_relations_multiple()))
        relations.sort()  # not necessary but makes debugging easier
        return relations
    
    def get_relations_multiple(self) -> List[str]:
        """
        Get the relations in the graph, including duplicate relations.
        """
        relations = [triplet[1] for triplet in self.g]
        return relations

    def __str__(self):
        out_str = '\n'.join([str(triplet) for triplet in self.g])        
        return out_str

class Data(Namespace):
    def __init__(self, **kwargs):
        super().__init__()
        self.__dict__.update(kwargs)

def get_dummy_graph(num_triplets:int=3) -> Graph:
    g = [
        ("dog", "IsA", "animal"),
        ("cat", "IsA", "animal"),
        ("black poodle", "IsA", "dog"),
        ("black cat", "IsA", "cat"),
    ]
    assert num_triplets <=4, "num_triplets must be <= 4"
    g = g[:num_triplets]
    g = Graph(g)
    return g

def r2nl(r: str) -> str:
    """
    Convert a relation to a natural language string. Can be used to implement necessary changes in the data.
    """
    return r

def _get_str2tok(g:Graph, tokenizer: T5Tokenizer) -> dict[str, list[int]]:
    """
    Get a dictionary that maps strings to tokens.
    """
    # tokenize concepts and relations
    c_tok = tokenizer([r2nl(c) for c in g.concepts], padding=False)['input_ids']
    r_tok = tokenizer([r2nl(r) for r in g.relations], padding=False)['input_ids']

    tokens = c_tok + r_tok
    node_names = g.concepts + g.relations  # these are not necessarily all nodes in the Levi Graph, as relations can occur more than once
    assert len(tokens) == len(node_names), f"{len(tokens) = }, {len(node_names) = }"

    # remove end-of-sequence token
    tokens = [toks[:-1] if toks[-1] == tokenizer.eos_token_id else toks for toks in tokens]
    
    # create a dictionary mapping concepts and relations to their tokenized forms
    str2tok = {node: tok for node, tok in zip(node_names, tokens)}
    str2tok['</s>'] = [tokenizer.eos_token_id]
    return str2tok

def _get_graphT5_input_sequence(g:Graph, str2tok:dict, use_eos:bool) -> Tuple[list, dict]:
    # get input sequence (i.e. sequence that will be fed into the model for this graph)
    all_nodes = g.relations_multiple + g.concepts  # list of all concepts and relations that will be in the final sequence (i.e. all nodes of the Levi Graph)  # the order of nodes is first all relations (in the order that they appear in g.g), and then all concepts (in alphabetical order. though here the order is not important)
    
    if use_eos:
        all_nodes.append('</s>')

    all_tokens = [str2tok[node] for node in all_nodes]  # list of length #nodes, where each element is a list of token ids
    indices = {node: [] for node in all_nodes}  # dictionary mapping each node to its start-index and end- in the sequence. Keys are nodes, values are lists of tuples (start_index, end_index). The lists have a length of 1 for concepts and are as long as the number of occurances of the relation in the graph for relations.  # WARNING: this assumes that concepts and realtions have different names. This not always the case for REBEL. For concept_indices this is fixed. 
    num_relation_tokens = sum([len(token) for token in all_tokens[:len(g.relations_multiple)]])  # number of tokens that are relations
    num_concept_tokens = sum([len(token) for token in all_tokens[len(g.relations_multiple):len(g.relations_multiple)+len(g.concepts)]])  # number of tokens that are concepts
    num_eos_tokens = 1 if use_eos else 0

    is_concept = torch.tensor([False] * num_relation_tokens + [True] * num_concept_tokens + [False] * num_eos_tokens, dtype=torch.bool)  # tensor of length #nodes, where each element is True if the node is a concept and False if it is a relation
    index_counter = 0
    assert len(all_nodes) == len(all_tokens), (all_nodes, all_tokens)

    for node, token in zip(all_nodes, all_tokens):
        indices[node].append((index_counter, index_counter + len(token)))
        # assert is_concept[index_counter:index_counter+len(token)].all() == (node in g.concepts), f"{is_concept = }, {node = }, {g.concepts = }, {index_counter = }, {len(token) = }, {is_concept[index_counter:index_counter+len(token)] = }"
        index_counter += len(token)

    concept_indices = {node: [indices[node][-1]] for node in g.concepts}  # [-1] and reput in list in case relations have the same name as a concept (concepts are put in last). 
    sequence = torch.tensor(list(chain.from_iterable(all_tokens)), dtype=torch.long)
    sequence = sequence.unsqueeze(0)  # add batch dimension
    is_concept = is_concept.unsqueeze(0)  # add batch dimension
    return sequence, indices, is_concept, concept_indices

def _get_graphT5_relativeposition_sparsitymask(g:Graph, indices:dict, sequence_length:int, use_eos:bool, eos:str) -> Tuple[torch.Tensor, torch.Tensor]:
    ### get relative position of each node in the sequence, as well as the sparsity mask ###
    # initialize relative position matrix)
    relative_position = torch.zeros(size=(sequence_length, sequence_length), dtype=torch.long) 
    # initialize sparsity mask
    sparsity_mask = torch.zeros(size=(sequence_length, sequence_length), dtype=torch.bool)
    # initialize use_additional_bucket
    use_additional_bucket = torch.zeros(size=(sequence_length, sequence_length), dtype=torch.bool)
    
    # relative positions / sparsity within each node
    for start, end in chain.from_iterable(indices.values()):
        relative_position[start:end, start:end] = _get_relative_position(end-start)
        sparsity_mask[start:end, start:end] = True

    # relative position between nodes of the same triplet
    relation_counter = {relation: 0 for relation in g.relations}  # dictionary mapping each relation to the number of times it has already appeared in the graph
    for triplet in g.g:
        pos_h = indices[triplet[0]][0]  # position of head; tuple (start_index, end_index)
        pos_r = indices[triplet[1]][relation_counter[triplet[1]]]  # position of relation; tuple (start_index, end_index)
        pos_t = indices[triplet[2]][0]  # position of tail; tuple (start_index, end_index)
        
        l_h, l_r = pos_h[1] - pos_h[0], pos_r[1] - pos_r[0]  # length (i.e. number of tokens) of head and relation

        # iterate over all combinations of tokens in each triplet. This implementation is not very elegant, but it is sufficiently fast.
        for ih, ph in enumerate(range(pos_h[0], pos_h[1])):  # iterate over all head tokens
            for ir, pr in enumerate(range(pos_r[0], pos_r[1])):  # iterate over all relation tokens
                relative_position[ph, pr] = l_h - ih + ir
                relative_position[pr, ph] = - (l_h - ih + ir)
                sparsity_mask[ph, pr] = True
                sparsity_mask[pr, ph] = True
            for it, pt in enumerate(range(pos_t[0], pos_t[1])):  # iterate over all tail tokens
                relative_position[ph, pt] = l_h - ih + l_r + it
                relative_position[pt, ph] = - (l_h - ih + l_r + it)
                sparsity_mask[ph, pt] = True
                sparsity_mask[pt, ph] = True
        for ir, pr in enumerate(range(pos_r[0], pos_r[1])):  # iterate over all relation tokens
            for it, pt in enumerate(range(pos_t[0], pos_t[1])):  # iterate over all tail tokens
                relative_position[pr, pt] = l_r - ir + it
                relative_position[pt, pr] = - (l_r - ir + it)
                sparsity_mask[pr, pt] = True
                sparsity_mask[pt, pr] = True

        relation_counter[triplet[1]] += 1  # next time when that relation comes, then the next tokens will be used
    
    if use_eos:
        assert len(indices['</s>']) == 1, f"{indices['</s>'] = } should have length 1"
        pos_eos = indices['</s>'][0]  # position of head; tuple (start_index, end_index)
        assert pos_eos[0] + 1 == pos_eos[1], pos_eos
        pos_eos = pos_eos[0]  # position of eos token

        if eos == 'bidirectional':
            relative_position[:, pos_eos] = +1e6
            relative_position[pos_eos, :] = -1e6
            relative_position[pos_eos, pos_eos] = 0
            sparsity_mask[:, pos_eos] = True
            sparsity_mask[pos_eos, :] = True
        elif eos == 'unidirectional':
            relative_position[:, pos_eos] = 1e6
            relative_position[pos_eos, pos_eos] = 0
            sparsity_mask[pos_eos, :] = False  # no messages from eos to other tokens
            sparsity_mask[:, pos_eos] = True
        else:
            raise ValueError(f'{eos = } is not a valid option.')
    
    relative_position = relative_position.unsqueeze(0)  # add batch dimension
    sparsity_mask = sparsity_mask.unsqueeze(0)  # add batch dimension
    use_additional_bucket = use_additional_bucket.unsqueeze(0)  # add batch dimension
    return relative_position, sparsity_mask, use_additional_bucket

def _get_global_graphT5_relativeposition_sparsitymask(g:Graph, indices:dict, sequence_length:int, use_eos:bool, eos:str) -> Tuple[torch.Tensor, torch.Tensor]:
    ### get relative position of each node in the sequence, as well as the sparsity mask ###
    # initialize relative position matrix)
    # relative_position = torch.ones(size=(sequence_length, sequence_length), dtype=torch.long) * 1e6  # technically should be float('inf'), but it does not matter
    relative_position = torch.zeros(size=(sequence_length, sequence_length), dtype=torch.long)
    # initialize sparsity mask
    sparsity_mask = torch.ones(size=(sequence_length, sequence_length), dtype=torch.bool)  # could switch to None, but then code has to be updated accordingly (in particular get_batch)
    # initialize use_additional_bucket
    use_additional_bucket = torch.ones(size=(sequence_length, sequence_length), dtype=torch.bool)

    # relative positions / sparsity within each node
    for start, end in chain.from_iterable(indices.values()):
        relative_position[start:end, start:end] = _get_relative_position(end-start)
        use_additional_bucket[start:end, start:end] = False

    # relative position between nodes of the same triplet
    relation_counter = {relation: 0 for relation in g.relations}  # dictionary mapping each relation to the number of times it has already appeared in the graph
    for triplet in g.g:
        pos_h = indices[triplet[0]][0]  # position of head; tuple (start_index, end_index)
        pos_r = indices[triplet[1]][relation_counter[triplet[1]]]  # position of relation; tuple (start_index, end_index)
        pos_t = indices[triplet[2]][0]  # position of tail; tuple (start_index, end_index)
        
        l_h, l_r = pos_h[1] - pos_h[0], pos_r[1] - pos_r[0]  # length (i.e. number of tokens) of head and relation

        # iterate over all combinations of tokens in each triplet. This implementation is not very elegant, but it works.
        for ih, ph in enumerate(range(pos_h[0], pos_h[1])):  # iterate over all head tokens
            for ir, pr in enumerate(range(pos_r[0], pos_r[1])):  # iterate over all relation tokens
                relative_position[ph, pr] = l_h - ih + ir
                relative_position[pr, ph] = - (l_h - ih + ir)
                use_additional_bucket[ph, pr] = False
                use_additional_bucket[pr, ph] = False
            for it, pt in enumerate(range(pos_t[0], pos_t[1])):  # iterate over all tail tokens
                relative_position[ph, pt] = l_h - ih + l_r + it
                relative_position[pt, ph] = - (l_h - ih + l_r + it)
                use_additional_bucket[ph, pt] = False
                use_additional_bucket[pt, ph] = False
        for ir, pr in enumerate(range(pos_r[0], pos_r[1])):  # iterate over all relation tokens
            for it, pt in enumerate(range(pos_t[0], pos_t[1])):  # iterate over all tail tokens
                relative_position[pr, pt] = l_r - ir + it
                relative_position[pt, pr] = - (l_r - ir + it)
                use_additional_bucket[pr, pt] = False
                use_additional_bucket[pt, pr] = False

        relation_counter[triplet[1]] += 1  # next time when that relation comes, then the next tokens will be used
        if use_eos:
            assert len(indices['</s>']) == 1, f"{indices['</s>'] = } should have length 1"
            pos_eos = indices['</s>'][0]  # position of head; tuple (start_index, end_index)
            assert pos_eos[0] + 1 == pos_eos[1], pos_eos
            pos_eos = pos_eos[0]  # position of eos token

            if eos == 'bidirectional':
                relative_position[:, pos_eos] = +1e6
                relative_position[pos_eos, :] = -1e6
                relative_position[pos_eos, pos_eos] = 0
                sparsity_mask[:, pos_eos] = True
                sparsity_mask[pos_eos, :] = True
                use_additional_bucket[:, pos_eos] = False
                use_additional_bucket[pos_eos, :] = False
            elif eos == 'unidirectional':
                relative_position[:, pos_eos] = 1e6
                relative_position[pos_eos, pos_eos] = 0
                sparsity_mask[pos_eos, :] = False  # no messages from eos to other tokens
                sparsity_mask[:, pos_eos] = True
                use_additional_bucket[:, pos_eos] = False
                use_additional_bucket[pos_eos, :] = False
            else:
                raise ValueError(f'{eos = } is not a valid option.')
    
    relative_position = relative_position.unsqueeze(0)  # add batch dimension
    sparsity_mask = sparsity_mask.unsqueeze(0)  # add batch dimension
    use_additional_bucket = use_additional_bucket.unsqueeze(0)  # add batch dimension
    return relative_position, sparsity_mask, use_additional_bucket

def graph_to_graphT5(g:Graph, tokenizer:T5Tokenizer, how:str, eos:str)->Data:
    """
    Convert a graph to a graphT5 input.
    :param g: graph
    :param tokenizer: tokenizer
    :param how: how to represent the graph. Can be 'local' or 'global' for lGLM and gGLM respectively.
    :param eos: end-of-sequence token. Can be `False` for not using an eos token. When using an eos token, there are two ways to use it: `bidirectional` means that the eos token is connected to every other node in the graph, with a relative position of positive infinity (from node to eos) or negative infinity (from eos to node). `unidirectional` means that the eos token is connected to every node in the graph with a relative position of positive infinity (from node to eos), but not the other way around (i.e. no connection from eos to other node). This means, that nodes do not get messages from the eos token, which perceives locality when using the local GLM
    """
    if not isinstance(g, Graph):
        g = Graph(g)
    eos = str(eos)
    assert eos in ['False', 'bidirectional', 'unidirectional'], f"{eos = } must be either 'False', 'bidirectional', or 'unidirectional'"
    use_eos:bool = eos != 'False'

    str2tok = _get_str2tok(g, tokenizer)  # get a dictionary mapping concepts and relations to their tokenized forms

    sequence, indices, is_concept, concept_indices = _get_graphT5_input_sequence(g, str2tok, use_eos)  # get input sequence (i.e. sequence that will be fed into the model for this graph
    sequence_length = sequence.shape[1]

    if how == 'local':
        relative_position, sparsity_mask, use_additional_bucket = _get_graphT5_relativeposition_sparsitymask(g, indices, sequence_length, use_eos, eos)
        num_additional_buckets = 0  # lGLM does not use additional buckets
    elif how == 'global':
        relative_position, sparsity_mask, use_additional_bucket = _get_global_graphT5_relativeposition_sparsitymask(g, indices, sequence_length, use_eos, eos)
        num_additional_buckets = 1  # gGLM uses 1 additional bucket for long-ranged G2G connections
    else:
        raise ValueError(f"how must be either 'local' or 'global', but is {how}")

    input_ids = sequence

    data = Data(input_ids=input_ids, relative_position=relative_position, sparsity_mask=sparsity_mask, use_additional_bucket=use_additional_bucket, indices=indices, is_concept=is_concept, concept_indices=concept_indices, num_additional_buckets=num_additional_buckets)

    return data

@cache
def _get_relative_position(size):
    return torch.tensor([[i - j for i in range(size)] for j in range(size)], dtype=torch.long)

def get_embedding(
        sequence_embedding: torch.Tensor,
        indices: Dict[str, List[Tuple[int, int]]],
        concept: str,
        embedding_aggregation: str = "mean",
    ):
    """
    Returns the embedding of a concept.
    :param sequence_embedding: the embedding of the whole sequence. shape: (sequence_length, embedding_size)
    :param indices: dictionary mapping each node to its start-index and end- in the sequence. Keys are nodes, values are lists of tuples (start_index, end_index). The lists have a length of 1 for concepts.
    :param concept: the concept for which the embedding should be returned
    :param embedding_aggregation: how the embedding of a concept should be aggregated. Either "mean" or "seq". "mean" returns the mean of all tokens of the concept. "seq" returns the embeddings of the all token of the concept.
    :return: the aggregated embedding of the concept. shape (1, embedding_size) or (number_of_tokens, embedding_size). 
    """
    assert concept in indices.keys(), f"{concept = } is not a node in the graph. {indices = }"
    assert len(indices[concept]) == 1, f"{concept = } is not a concept, as concepts occur only once in the graph. {indices = }"

    start, end = indices[concept][0]
    sequence_embedding = sequence_embedding[start:end, :]
    if embedding_aggregation == "mean":
        return torch.mean(sequence_embedding, dim=0, keepdim=True)
    elif embedding_aggregation == "seq":
        return sequence_embedding
    else:
        raise NotImplementedError(f"{embedding_aggregation = } is not supported. Use either 'mean' or 'seq'.")

def add_text_to_graph_data(data, text, tokenizer, use_text):
    if use_text in {'False', '', False, None}:
        return None

    text_seq = torch.tensor(tokenizer(text, padding=False)['input_ids']).unsqueeze(0)
    new_input_ids = torch.cat([data.input_ids, text_seq], dim=1)

    old_seq_len = data.input_ids.shape[1]
    text_seq_len = text_seq.shape[1]
    new_seq_len = new_input_ids.shape[1]

    new_is_graph = torch.zeros(size=(1, new_seq_len), dtype=torch.bool)
    new_is_graph[:, :old_seq_len] = True

    if data.relative_position is None:  # sequence transformer
        assert data.sparsity_mask is None
        assert data.use_additional_bucket is None
        data.input_ids = new_input_ids
        data.is_graph = new_is_graph
        return None

    new_relative_position = torch.zeros(size=(1, new_seq_len, new_seq_len), dtype=data.relative_position.dtype)
    new_relative_position[:, :old_seq_len, :old_seq_len] = data.relative_position
    new_relative_position[:, old_seq_len:, old_seq_len:] = _get_relative_position(text_seq_len)

    new_sparsity_mask = torch.zeros(size=(1, new_seq_len, new_seq_len), dtype=data.sparsity_mask.dtype)
    new_sparsity_mask[:, :old_seq_len, :old_seq_len] = data.sparsity_mask
    new_sparsity_mask[:, old_seq_len:, old_seq_len:] = True
    
    new_use_additional_bucket = torch.zeros(size=(1, new_seq_len, new_seq_len), dtype=data.use_additional_bucket.dtype)
    new_use_additional_bucket[:, :old_seq_len, :old_seq_len] = data.use_additional_bucket
    new_use_additional_bucket[:, old_seq_len:, old_seq_len:] = False  # could change that if we want T2T and local G2G relations to be learned separately

    if use_text in {'FullyConnected', True}:
        new_sparsity_mask[:, old_seq_len:, :old_seq_len] = True
        new_sparsity_mask[:, :old_seq_len, old_seq_len:] = True

        new_use_additional_bucket[:, old_seq_len:, :old_seq_len] = True
        new_use_additional_bucket[:, :old_seq_len, old_seq_len:] = True

        new_relative_position[:, old_seq_len:, :old_seq_len] = data.num_additional_buckets
        new_relative_position[:, :old_seq_len, old_seq_len:] = data.num_additional_buckets + 1
        
        new_num_additional_buckets = data.num_additional_buckets + 2
    else:
        raise ValueError(f"unknown use_text {use_text} (type {type(use_text)})")

    data.input_ids = new_input_ids
    data.relative_position = new_relative_position
    data.sparsity_mask = new_sparsity_mask
    data.use_additional_bucket = new_use_additional_bucket
    data.num_additional_buckets = new_num_additional_buckets
    data.is_graph = new_is_graph
    return None

class DataProcessor():
    @staticmethod
    def encode_graph(tokenizer, g:Union[Graph,list[tuple[str,str,str]]], text:Optional[str]=None, how:Literal['global', 'local']='global', eos:str="False")->Data:
        """
        convert graph to suitable input for the model. 
        :param tokenizer: tokenizer
        :param g: graph
        :param text: text to add to the graph. Can be None if no text should be added. 
        :param how: how to represent the graph. Can be 'local' or 'global' for lGLM and gGLM respectively.
        :param eos: end-of-sequence token. Can be `False` for not using an eos token. This is the method used in the paper. When using an eos token, there are two ways to use it: `bidirectional` means that the eos token is connected to every other node in the graph. `unidirectional` means that the eos token is connected to every node in the graph (from node to eos), but not the other way around (i.e. no connection from eos to other node). This means, that nodes do not get messages from the eos token, which perceives locality when using the local GLM
        :return: Data object
        """
        if not isinstance(g, Graph):
            g = Graph(g)
        data = graph_to_graphT5(g, tokenizer, how, eos)
        if text is not None:
            add_text_to_graph_data(data, text, tokenizer, use_text=True)
        return data
    
    @staticmethod
    def to_batch(data_instances:list[Data], tokenizer, max_seq_len:Optional[int]=None, device:str='cpu', **kwargs)->dict: 
        """
        converts list of data instances to batched inputs for GLM forward call. 
        :param datas: list of Data instances
        :param max_seq_len: maximum sequence length
        :param tokenizer: tokenizer
        :param device: device
        :return: dictionary with keys 'input_ids', 'relative_position', 'sparsity_mask', and 'use_additional_bucket'
        """
        current_max_seq_len = max([data.input_ids.shape[1] for data in data_instances])
        if max_seq_len is None:
            max_seq_len = current_max_seq_len
        else:
            max_seq_len = min(max_seq_len, current_max_seq_len)

        if data_instances[0].relative_position is None:
            assert data_instances[0].sparsity_mask is None
            assert data_instances[0].use_additional_bucket is None
            is_sequence_transformer = True
        else:
            assert data_instances[0].sparsity_mask is not None
            assert data_instances[0].use_additional_bucket is not None
            is_sequence_transformer = False

        # intialize tensors
        input_ids = torch.ones((len(data_instances), max_seq_len), dtype=torch.long, device=device) * tokenizer.pad_token_id
        if is_sequence_transformer:
            relative_position = None 
            sparsity_mask = None 
            use_additional_bucket = None 
        else:
            relative_position = torch.zeros((len(data_instances), max_seq_len, max_seq_len), dtype=torch.long, device=device)
            sparsity_mask = torch.zeros((len(data_instances), max_seq_len, max_seq_len), dtype=torch.bool, device=device)
            use_additional_bucket = torch.zeros((len(data_instances), max_seq_len, max_seq_len), dtype=torch.bool, device=device)

        # fill tensors
        for i, data in enumerate(data_instances):
            instance_len = min(data.input_ids.shape[1], max_seq_len)
            input_ids[i, :instance_len] = data.input_ids[:, :instance_len]
            if not is_sequence_transformer:
                relative_position[i, :instance_len, :instance_len] = data.relative_position[:, :instance_len, :instance_len]
                sparsity_mask[i, :instance_len, :instance_len] = data.sparsity_mask[:, :instance_len, :instance_len]
                use_additional_bucket[i, :instance_len, :instance_len] = data.use_additional_bucket[:, :instance_len, :instance_len]

        model_input = {
            'input_ids': input_ids,
            'relative_position': relative_position,
            'sparsity_mask': sparsity_mask,
            'use_additional_bucket': use_additional_bucket,
            **kwargs
        }
        return model_input

    @staticmethod
    def get_embedding(sequence_embedding:torch.Tensor, indices:Dict[str,List[Tuple[int, int]]], concept:str, embedding_aggregation:str="mean"):
        """
        Returns embedding of a concept.
        :param sequence_embedding: the embedding of the whole sequence. shape: (sequence_length, embedding_size)
        :param indices: dictionary mapping each node to its start- and end-index in the sequence. Keys are nodes, values are lists of tuples (start_index, end_index). The lists have a length of 1 for concepts. indices is part of the Data object. 
        :param concept: the concept for which the embedding should be returned. 
        :param embedding_aggregation: how the embedding of a concept should be aggregated. Either "mean" or "seq". "mean" returns the mean of all tokens of the concept. "seq" returns the embeddings of the all token of the concept.
        :return: the aggregated embedding of the concept. shape (1, embedding_size) or (number_of_tokens, embedding_size).
        """
        return get_embedding(sequence_embedding, indices, concept, embedding_aggregation)