def test_vector_edge_loss(self): nodes = [Node(), Node(), Node()] edges = [ Edge(nodes[0], nodes[1], Attribute(Tensor([-50., -10., -5.]))), Edge(nodes[1], nodes[0], Attribute(Tensor([-40., 100., 120.]))), Edge(nodes[0], nodes[2], Attribute(Tensor([1., 3., 4.]))), Edge(nodes[0], nodes[0], Attribute(Tensor([2., 2., 2.]))) ] g1 = Graph(nodes, edges) g2 = deepcopy(g1) g2.ordered_edges[0].attr.val = Tensor([-45., -11., -5.]) g2.ordered_edges[1].attr.val = Tensor([-40., 200., 121.]) g2.ordered_edges[2].attr.val = Tensor([1.1, 3., 3.9]) g2.ordered_edges[3].attr.val = Tensor([2., 2., 2.1]) loss = GraphLoss(e_fn=MSELoss()) loss_val = loss(g1, g2).detach().numpy() # division by 3 because there are three entries per vector # division by 4 because there are four edges target_loss_val = ((-50. + 45.)**2 + (-10. + 11.)**2 + (-40. + 40.)**2 + (100. - 200.)**2 + (120. - 121.)**2 + (1 - 1.1)**2 + (4. - 3.9)**2 + (2 - 2.1)**2) / 3 / 4 self.assertTrue(np.isclose(loss_val, target_loss_val))
def _create_tuple(x_min: int, x_max: int) -> Tuple[Graph, Graph]: """ Creates a sorting task training sample, i.e. a tuple consisting of (input, target). :param x_min: Number min value :param x_max: Number max value (exclusive) :return: Two graphs, both fully connected, one with randomly initialized attributes, the other one with targets. """ assert x_min < x_max vec = torch.arange(x_min, x_max) nodes = [Node(Attribute(x)) for x in vec] g_in = Graph(nodes, []) g_out = deepcopy(g_in) def sorted_edge_attribute_generator(n1: Node, n2: Node) -> Attribute: val = [0, 1] if n1.attr.val < n2.attr.val else [1, 0] return Attribute(torch.Tensor(val).float()) g_in.add_all_edges( reflexive=False, attribute_generator=lambda n1, n2: Attribute(torch.randn(2))) g_out.add_all_edges( reflexive=False, attribute_generator=sorted_edge_attribute_generator) return g_in, g_out
def test_combined_loss(self): nodes = [ Node(Attribute(Tensor([-4., -8.]))), Node(Attribute(Tensor([1., 5.]))), Node(Attribute(Tensor([4., 4.]))), Node(Attribute(Tensor([0., 1., 5.]))) ] edges = [ Edge(nodes[0], nodes[1], Attribute(Tensor([1., 2., 3.]))), Edge(nodes[1], nodes[2], Attribute(Tensor([1., 2.]))), Edge(nodes[2], nodes[1], Attribute(Tensor([5.]))), Edge(nodes[1], nodes[3], Attribute(Tensor([1., 2., 3., 4.]))) ] u = Attribute( Tensor([[1., 2., 4., 3.], [8., 3., 0., 3.], [1., 7., 5., 3.]])) g1 = Graph(nodes, edges, attr=u) g2 = deepcopy(g1) g2.ordered_nodes[0].attr.val = Tensor([-4., -8.1]) g2.ordered_nodes[1].attr.val = Tensor([2., 6.]) g2.ordered_nodes[3].attr.val = Tensor([1., 1.5, 5.]) g2.ordered_edges[0].attr.val = Tensor([2., 3., 4.]) g2.ordered_edges[1].attr.val = Tensor([5., 10.]) g2.attr.val = Tensor([[2., 2., 4., 3.], [100, 3., 1., 3.], [1., 14., 5., 3.]]) loss = GraphLoss(e_fn=MSELoss(), v_fn=L1Loss(), u_fn=MSELoss()) loss_val = loss(g1, g2).detach().numpy() e_loss = (1. + (4**2 + 8**2) / 2) / 4 v_loss = (.1 / 2 + 2. / 2 + (1 + .5) / 3) / 4 u_loss = (1 + (8 - 100)**2 + 1 + 7**2) / 12 / 1 target_loss_val = v_loss + e_loss + u_loss self.assertTrue(np.isclose(loss_val, target_loss_val))
def forward(self, aggr_e: Attribute, aggr_v: Attribute, u: Attribute) -> Attribute: x = torch.cat((aggr_e.val, aggr_v.val, u.val)) x = F.relu(self.dense(x)) x = F.dropout(x, p=self.drop_p, training=self.training) return Attribute(x)
def forward(self, e: Attribute, v_r: Attribute, v_s: Attribute, u: Attribute) -> Attribute: x = torch.cat((e.val, v_r.val, v_s.val, u.val)) x = F.relu(self.dense(x)) x = F.dropout(x, p=self.drop_p, training=self.training) return Attribute(x)
def add_reflexive_edges(self, attribute_generator: Optional[Callable[[Node], Attribute]] = None): if attribute_generator is None: attribute_generator = lambda _: Attribute() for n in self.nodes: e = Edge(n, n, attribute_generator(n)) self.add_edge(e)
def _feed(aggr: Aggregation, in_val: List[List[float]], out_target: List[float]) -> bool: in_attrs = [Attribute(torch.Tensor(np.array(x))) for x in in_val] out_target = np.array(out_target) out_tensor: torch.Tensor = aggr(in_attrs).val out_val = out_tensor.detach().numpy() return np.allclose(out_val, out_target)
def test_scalar_edge_loss(self): nodes = [Node(), Node(), Node()] edges = [ Edge(nodes[0], nodes[1], Attribute(Tensor([-50.]))), Edge(nodes[1], nodes[0], Attribute(Tensor([-40.]))), Edge(nodes[0], nodes[2], Attribute(Tensor([1.]))) ] g1 = Graph(nodes, edges) g2 = deepcopy(g1) g2.ordered_edges[0].attr.val = Tensor([-45.]) g2.ordered_edges[1].attr.val = Tensor([-40.]) g2.ordered_edges[2].attr.val = Tensor([1.1]) loss = GraphLoss(e_fn=MSELoss()) loss_val = loss(g1, g2).detach().numpy() target_loss_val = ((-50. + 45.)**2 + (-40. + 40.)**2 + (1 - 1.1)**2) / 3 self.assertTrue(np.isclose(loss_val, target_loss_val))
def test_identity_property(self): nodes = [ Node(Attribute([1, 23, 4])), Node(Attribute("stringattr")), Node(Attribute([1])), Node(Attribute(5)) ] edges = [ Edge(nodes[0], nodes[1], Attribute({'dict': 1234})), Edge(nodes[0], nodes[1], Attribute([1, 2, 3])), Edge(nodes[0], nodes[2], Attribute(5)), Edge(nodes[1], nodes[2], Attribute([3, 4, 5])), Edge(nodes[1], nodes[1], Attribute()) ] global_state = Attribute([[1, 2, 3], [5, 6, 7]]) g1 = Graph(nodes, edges, global_state) g1_prime = Graph.from_dict(g1.asdict()) self.assertTrue(g1 == g1_prime)
def forward(self, attrs: List[Attribute]) -> Attribute: n = len(attrs) assert n > 0, "Maximum aggregation cannot be applied to empty lists." attr_vals = [a.val for a in attrs] attr_vals = torch.stack(attr_vals) attr_vals_max = torch.max( attr_vals, dim=0, keepdim=False)[0] # returns (Tensor, LongTensor) return Attribute(attr_vals_max)
def forward(self, attrs: List[Attribute]) -> Attribute: n = len(attrs) assert n > 0, "Average aggregation cannot be applied to empty lists." attr_vals = [a.val for a in attrs] attr_vals = torch.stack(attr_vals) attr_vals_sum = torch.sum(attr_vals, dim=0, keepdim=False) attr_vals_avg = attr_vals_sum / n return Attribute(attr_vals_avg)
def forward(self, aggr_e: Attribute, v: Attribute, u: Attribute) -> Attribute: img_tensor = v.val assert aggr_e.val.is_cuda, "Following reshaping does only work on GPU" aggr_channels = aggr_e.val.unsqueeze(-1).unsqueeze( -1) # add height and width dimension for broadcasting img_plus_aggr = torch.add(img_tensor, aggr_channels) / 2 conv_out = self.dense_layers(img_plus_aggr) return Attribute(conv_out)
def test_forward_pass(self): linear_block = LinearIndependentGNBlock(e_config=(4, 8, True), v_config=(1, 1, False), u_config=(16, 16, True)) nodes = Node.from_vals( [torch.randn(1), torch.randn(1), torch.randn(1)]) edges = [ Edge(nodes[0], nodes[1], Attribute(torch.randn(4))), Edge(nodes[1], nodes[2], Attribute(torch.randn(4))), Edge(nodes[2], nodes[1], Attribute(torch.randn(4))) ] g_in = Graph(nodes, edges, Attribute(torch.randn(16))) # noinspection PyUnusedLocal g_out = linear_block(g_in) self.assertTrue( True ) # the assertion is that the forward pass works without errors
def forward(self, attrs: List[Attribute]) -> Attribute: assert len( attrs) > 0, "Sum aggregation cannot be applied to empty lists" in_shape = attrs[0].val.shape attr_vals = [a.val.view(-1) for a in attrs] attr_vals_concat = torch.stack(attr_vals) attr_vals_sum = torch.sum(attr_vals_concat, dim=0, keepdim=False) assert attr_vals_sum.shape == in_shape return Attribute(attr_vals_sum)
def load_graph(self, path: str) -> Graph: # read JSON from page description file json_file_path = glob(os.path.join(path, '*.json')) assert len(json_file_path) == 1, "Number of json files in '{}' must be exactly one.".format(path) json_file_path = json_file_path[0] with open(json_file_path, encoding='utf-8') as json_file: pages_json: List = json.load(json_file) # read screenshot paths imgs = self.load_images(os.path.join(path, 'image')) assert len(pages_json) == len(imgs), "Number of pages and number of screenshots mismatch in '{}'.".format(path) # extract nodes nodes = {} for page_json in pages_json: desktop_img, mobile_img = imgs[page_json['id']-1] node_attribute = PageAttribute.from_json(page_json, desktop_img, mobile_img) url = page_json['base_url'] if url in nodes: logging.debug("Found two nodes with the same URL.") continue node = Node(node_attribute) nodes[url] = node # extract edges edges = set() for page_json in pages_json: url = page_json['base_url'] source_node = nodes[url] for edge_json in page_json.get('urls', []): target_url = edge_json['url'] if target_url not in nodes: logging.debug("Invalid link target URL. Could not find a node that corresponds to it.") continue target_node = nodes[target_url] edge_attribute = LinkAttribute(target_url) edge = Edge(sender=source_node, receiver=target_node, attr=edge_attribute) edges.add(edge) return Graph(nodes=list(nodes.values()), edges=list(edges), attr=Attribute(None))
def test_learn_identity(self): # construct input graph with random values nodes = Node.from_vals( [torch.randn(1), torch.randn(1), torch.randn(1)]) edges = [ Edge(nodes[0], nodes[1], Attribute(torch.randn(4))), Edge(nodes[1], nodes[2], Attribute(torch.randn(4))), Edge(nodes[2], nodes[1], Attribute(torch.randn(4))) ] g_in = Graph(nodes, edges, Attribute(torch.randn(16))) g_target = deepcopy(g_in) block_1 = LinearIndependentGNBlock(e_config=(4, 8, True), v_config=(1, 12, True), u_config=(16, 16, True)) block_2 = LinearIndependentGNBlock(e_config=(8, 4, False), v_config=(12, 1, False), u_config=(16, 16, False)) model = torch.nn.Sequential(block_1, block_2) opt = optim.SGD(model.parameters(), lr=.1, momentum=0) loss_fn = GraphLoss(e_fn=MSELoss(), v_fn=MSELoss(), u_fn=MSELoss()) loss = torch.Tensor([1.]) for step in range(100): model.train() opt.zero_grad() g_out = model(g_in) loss = loss_fn(g_out, g_target) loss.backward() opt.step() final_loss = loss.detach().numpy() print(final_loss) self.assertTrue(final_loss < 1e-3)
def add_all_edges(self, reflexive: bool = True, attribute_generator: Optional[Callable[[Node, Node], Attribute]] = None) -> None: """ Modifies the graph in-place, such that it is fully connected, adding n^n edges, where n is the number of nodes. :param reflexive: Whether to connect nodes to themselves :param attribute_generator: New edges will be given an attribute generated by the attribute generator """ if attribute_generator is None: attribute_generator = lambda sn, rn: Attribute() for n1 in self.nodes: for n2 in self.nodes: if not reflexive and n1 == n2: continue e = Edge(n1, n2, attribute_generator(n1, n2)) self.add_edge(e)
def __init__(self, nodes: List[Node], edges: List[Edge], attr: Optional[Attribute] = None): if attr is None: attr = Attribute(val=None) assert isinstance(nodes, list) and isinstance(edges, list), "Nodes and edges must be lists" self.nodes = set(nodes) # V self.edges = set(edges) # E self.attr = attr # e # store ordered representations for easy alignment of two graphs with equal structure self.ordered_nodes = nodes self.ordered_edges = edges self.device: torch.device = None self._check_integrity()
def test_graph_equality(self): v_0, v_1, v_2 = Node(Attribute(0.)), Node(Attribute(1.)), Node( Attribute(2.)) vs = [v_0, v_1, v_2] # nodes es = [Edge(v_0, v_1), Edge(v_0, v_2)] # edges g_0 = Graph(nodes=vs, edges=es) v_0, v_1, v_2 = Node(Attribute(0.)), Node(Attribute(1.)), Node( Attribute(2.)) vs = [v_0, v_1, v_2] # nodes es = [Edge(v_0, v_1), Edge(v_0, v_2)] # edges g_1 = Graph(nodes=vs, edges=es) self.assertTrue(g_0 == g_1)
def forward(self, aggr_e: Attribute, aggr_v: Attribute, u: Attribute) -> Attribute: return Attribute(self.dense(u.val))
def from_dict(d: Dict) -> 'Graph': nodes_dict = {k: Node.from_dict(node_dict) for k, node_dict in d['nodes'].items()} nodes = list(nodes_dict.values()) edges = [Edge.from_dict(e, nodes_dict) for e in d['edges']] attr = Attribute.from_dict(d['attr']) return Graph(nodes, edges, attr)
def test_lists_equal_objects_comparator(self): l1 = [Node(Attribute(1)), Node(Attribute(2)), Node(Attribute("test"))] l2 = [Node(Attribute(1)), Node(Attribute(2)), Node(Attribute("test"))] self.assertTrue(lists_equal(l1, l2, comparator=Node.eq_attr))
def sorted_edge_attribute_generator(n1: Node, n2: Node) -> Attribute: val = [0, 1] if n1.attr.val < n2.attr.val else [1, 0] return Attribute(torch.Tensor(val).float())
def forward(self, aggr_e: Attribute, aggr_v: Attribute, u: Attribute) -> Attribute: return Attribute(aggr_e.val + aggr_v.val - u.val)
def test_sets_equal_objects_comparator(self): s1 = {Node(Attribute(1)), Node(Attribute(2)), Node(Attribute("test"))} s2 = {Node(Attribute(2)), Node(Attribute(1)), Node(Attribute("test"))} self.assertTrue(sets_equal(s1, s2, comparator=Node.eq_attr))
def forward(self, e: Attribute, v_r: Attribute, v_s: Attribute, u: Attribute) -> Attribute: return Attribute(global_avg_pool(v_s.val))
def test_basic(self): """ Basic test w/o PyTorch, all attributes are scalars, edges do not have attributes. Feeds a graph through a basic graph block twice and compares to the target values after both passes. """ # create data structure v_0, v_1, v_2 = Node(Attribute(1)), Node(Attribute(10)), Node( Attribute(20)) vs = [v_0, v_1, v_2] # nodes es = [Edge(v_0, v_1), Edge(v_0, v_2), Edge(v_1, v_2)] g_0 = Graph(nodes=vs, edges=es, attr=Attribute(0)) # create block w/ functions block = GNBlock(phi_e=SenderIdentityEdgeUpdate(), phi_v=EdgeNodeSumNodeUpdate(), phi_u=MixedGlobalStateUpdate(), rho_ev=ScalarSumAggregation(), rho_vu=ScalarSumAggregation(), rho_eu=ScalarSumAggregation()) g_1 = block(g_0) v_0, v_1, v_2 = Node(Attribute(1)), Node(Attribute(10 + 1)), Node( Attribute(20 + 11)) vs = [v_0, v_1, v_2] # nodes es = [ Edge(v_0, v_1, Attribute(1)), Edge(v_0, v_2, Attribute(1)), Edge(v_1, v_2, Attribute(10)) ] g_1_target = Graph(nodes=vs, edges=es, attr=Attribute(35)) self.assertTrue(g_1 == g_1_target) g_2 = block(g_1) v_0, v_1, v_2 = Node(Attribute(1)), Node(Attribute(10 + 2)), Node( Attribute(20 + 11 + 12)) vs = [v_0, v_1, v_2] # nodes es = [ Edge(v_0, v_1, Attribute(1)), Edge(v_0, v_2, Attribute(1)), Edge(v_1, v_2, Attribute(11)) ] g_2_target = Graph(nodes=vs, edges=es, attr=Attribute(1 + 12 + 43 - 35)) self.assertTrue(g_2 == g_2_target)
g: Graph = batch[0]['graph'].to(device) g_hash = int(batch[0]['rank']) assert g_hash not in cache for n in g.nodes: desktop_img: Tensor = n.attr.val['desktop_img'] mobile_img: Tensor = n.attr.val['mobile_img'] # desktop and mobile feature vector x1, x2 = net(desktop_img.unsqueeze(0), mobile_img.unsqueeze(0)) x: torch.Tensor = torch.cat((x1, x2), dim=1).view(-1) x = x.detach().cpu() x = torch.Tensor(x.data) del n.attr n.attr = Attribute(x) g.to(torch.device('cpu')) cache[g_hash] = g if len(cache) == 4000: save_cached_graphs(cache, file_ctr) file_ctr += 1 cache = {} save_cached_graphs(cache, file_ctr)
def forward(self, attrs: List[Attribute]) -> Attribute: return Attribute(self.constant_val)
def forward(self, attrs: List[Attribute]) -> Attribute: return Attribute(sum([a.val for a in attrs]))