├── .github └── workflows │ └── python-app.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── deepstochlog ├── __init__.py ├── _meta_program_suffix.pl ├── context.py ├── dataloader.py ├── dataset.py ├── inferences.py ├── logic.py ├── model.py ├── network.py ├── networkevaluation.py ├── nn_models.py ├── parser.py ├── rule.py ├── tabled_and_or_trees.py ├── tabled_tree_builder.py ├── term.py ├── trainer.py └── utils.py ├── examples ├── addition │ ├── __init__.py │ ├── addition.pl │ ├── addition.py │ ├── addition_data.py │ ├── addition_evaluate.py │ └── addition_timing.py ├── addition_simple │ ├── addition_simple.pl │ └── addition_simple.py ├── anbncn │ ├── __init__.py │ ├── anbncn.pl │ ├── anbncn.py │ ├── anbncn_data.py │ └── anbncn_evaluate.py ├── bracket │ ├── __init__.py │ ├── bracket.pl │ ├── bracket.py │ ├── bracket_all.pl │ ├── bracket_data.py │ └── bracket_evaluate.py ├── citeseer │ ├── base │ │ ├── citeseer.pl │ │ ├── citeseer.py │ │ └── citeseer_data.py │ ├── citeseer_utils.py │ ├── with_influence │ │ ├── citeseer_data_withinfluence.py │ │ ├── citeseer_influence.pl │ │ └── citeseer_influence.py │ ├── with_rule_weights │ │ ├── citeseer_data_withrules.py │ │ ├── citeseer_ruleweights.pl │ │ └── citeseer_ruleweights.py │ ├── with_rule_weights_xy │ │ ├── citeseer_data_withrules.py │ │ ├── citeseer_ruleweights.pl │ │ └── citeseer_ruleweights.py │ └── with_structure_learning │ │ ├── citeseer_data_struct.py │ │ ├── citeseer_struct.pl │ │ └── citeseer_struct.py ├── cora │ └── with_rule_weights │ │ ├── cora_data_withrules.py │ │ ├── cora_ruleweights.pl │ │ └── cora_ruleweights.py ├── data_utils.py ├── evaluate.py ├── experiment_utils.py ├── mathexpression │ ├── __init__.py │ ├── download_hwf.sh │ ├── mathexpression.pl │ ├── mathexpression.py │ ├── mathexpression_data.py │ ├── mathexpression_notabling.pl │ ├── mathexpression_tabled_vs_notabled.py │ ├── mathexpression_tabled_with_probabilities.py │ ├── mathexpression_with_probabilities.pl │ └── splits_with_valid │ │ ├── expr_test.json │ │ ├── expr_train.json │ │ └── expr_val.json ├── models.py └── wap │ ├── __init__.py │ ├── wap.pl │ ├── wap.py │ ├── wap_data.py │ ├── wap_evaluate.py │ └── wap_network.py ├── requirements.txt ├── setup.py └── tests ├── test_anbncn.py ├── test_bracket.py ├── test_citeseer.py ├── test_examples.py ├── test_parser.py └── test_tabled_tree_builder.py /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 3.8 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: "3.8" 23 | cache: 'pip' 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | pip install coverage flake8 pytest 29 | - name: Test with pytest 30 | run: | 31 | coverage run --source=deepstochlog/ setup.py test 32 | coverage xml 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .idea/ 132 | data/ 133 | notebooks/*.png 134 | notebooks/*/*.png 135 | 136 | examples/mathexpression/logs_temp/ 137 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.md 2 | exclude tests/test*.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeepStochLog 2 | 3 | DeepStochLog is a neuro-symbolic framework that combines grammars, logic, probabilities and neural networks. 4 | By writing a DeepStochLog program, one can train a neural network with the given background knowledge. 5 | One can express symbolic information about subsymbolic data in DeepStochLog and help train neural networks more efficiently this way. 6 | For example, if the training data is made up of handwritten digit images, and we know the sum of these digits but not the individual numbers, one can express this relation in DeepStochLog and train the neural networks much faster. 7 | 8 | DeepStochLog uses a stochastic logic approach to encoding the probabilistic logic, and is thus faster than and can deal with longer inputs than its sibling DeepProbLog in our experiments. 9 | 10 | ## Installation 11 | 12 | ### Installing SWI Prolog 13 | DeepStochLog requires [SWI Prolog](https://www.swi-prolog.org/build/PPA.html) to run. 14 | Run the following commands to install: 15 | ``` 16 | sudo apt-add-repository ppa:swi-prolog/stable 17 | sudo apt-get update 18 | sudo apt-get install swi-prolog 19 | ``` 20 | 21 | ### Installing DeepStochLog package 22 | To install DeepStochLog itself, run the following command: 23 | 24 | ``` 25 | pip install deepstochlog 26 | ``` 27 | 28 | ## Running the examples 29 | 30 | ### Local dependencies 31 | 32 | To see DeepStochLog in action, please first install [SWI Prolog](https://www.swi-prolog.org/build/PPA.html) (as explained about), 33 | as well as the requirements listed in `requirements.txt` 34 | ``` 35 | pip install -r requirements.txt 36 | ``` 37 | 38 | ### Datasets 39 | The datasets used in the tasks used to evaluate DeepStochLog can be found in our [initial release](https://github.com/ML-KULeuven/deepstochlog/releases/tag/0.0.1). 40 | 41 | ### Addition example 42 | 43 | To see DeepStochLog in action, navigate to `examples/addition` and run `addition.py`. 44 | 45 | The neural definite clause grammar specification is provided in `addition.pl`. 46 | The `addition(N)` predicate specifies/recognises that two handwritten digits *N1* and *N2* sum to *N*. 47 | The neural probability `nn(number, [X], Y, digit)` makes the neural network with name `number` (a MNIST classifier) label input image X with the digit Y. 48 | 49 | 50 | 51 | ## Credits & Paper citation 52 | 53 | If use this work in an academic context, please consider citing [the following paper](https://arxiv.org/abs/2106.12574): 54 | 55 | The paper is also accepted to [AAAI22](https://aaai.org/Conferences/AAAI-22/). 56 | Please cite that version of the paper when the proceedings are out. 57 | 58 | ``` 59 | @article{winters2021deepstochlog, 60 | title={Deepstochlog: Neural stochastic logic programming}, 61 | author={Winters, Thomas and Marra, Giuseppe and Manhaeve, Robin and De Raedt, Luc}, 62 | journal={arXiv preprint arXiv:2106.12574}, 63 | year={2021} 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /deepstochlog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ML-KULeuven/deepstochlog/aed95319411b8b5190b5b418b65b523b1436bcfd/deepstochlog/__init__.py -------------------------------------------------------------------------------- /deepstochlog/_meta_program_suffix.pl: -------------------------------------------------------------------------------- 1 | :- table solve/1. 2 | :- discontiguous skip_predicate/1. 3 | is_builtin(A) :- A \= true, predicate_property(A,built_in). 4 | include_atom(A) :- A =.. [Func | _], include_predicate(Func). 5 | 6 | 7 | 8 | %compound_to_list/2 translates a compound term to a python compatible list, where the first element is either: 9 | % - a functor, meaning that the list represent a predicate of that functor whose arguments are the following elements 10 | % - the term `list`, meaning that the list is a Prolog list, and the following elements are the elements of the list. 11 | compound_to_list(C,L):- var(C), L = ["_"]. 12 | compound_to_list(C,L):- is_list(C), maplist(compound_to_list,C,LC), L =["list"|LC]. 13 | compound_to_list(C,L):- \+is_list(C), \+ var(C), C =.. [Funct|Args], maplist(compound_to_list,Args,LArgs), atom_string(Funct,FS), L=[FS|LArgs]. 14 | 15 | 16 | % The solve predicate is the actual engine. It is a meta interpreter with tabling. 17 | % It prints clauses that has include_predicate in the head. 18 | solve(true) :- !. 19 | solve((A,B)) :- solve(A), solve(B). 20 | solve(H) :- include_atom(H), 21 | clause(H,B), 22 | solve(B), 23 | write_ground_clause(H,B), fail. %this is a failure-driven loop. It is the only loop-like behaviour that works properly with SWIPL tabling. 24 | solve(H) :- include_atom(H), 25 | clause(H,B), 26 | solve(B). 27 | solve(A) :- A \= (_,_), A \= true, \+ include_atom(A), call(A). 28 | 29 | % Top-entry point for the solver. It writes elements for queries with variables (e.g. using the "_" variable) 30 | execute_query(Query) :- copy_term(Query, OriginalQuery), 31 | term_variables(Query, Vars), 32 | (length(Vars, 0) -> foreach(solve(Query), true); 33 | foreach(solve(Query), write_ground_clause(OriginalQuery, Query))). 34 | 35 | 36 | % Print a single line of the table using a python compatible syntax 37 | write_ground_clause(Head, Body):- 38 | comma_list(Body,BL), 39 | include(include_atom, BL, BLL), 40 | (BLL=[] -> true; ( 41 | ConjBody =.. [conj|BLL], 42 | compound_to_list(Head,LHead), 43 | compound_to_list(ConjBody,LLBody), 44 | write_term([LHead,LLBody],[quoted(true)]), 45 | nl)). 46 | 47 | % The main/0 executes the queries, potentially writing the top queries entries in the tables. 48 | % Then it writes all the entries of the table to the output stream. 49 | main :- maplist(execute_query, [{query}]). 50 | 51 | -------------------------------------------------------------------------------- /deepstochlog/context.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Union 2 | 3 | import torch 4 | from torch import Tensor 5 | 6 | from deepstochlog.term import Term 7 | 8 | 9 | class Context: 10 | """ Represents the context of a query: maps logic terms to tensors """ 11 | 12 | def __init__(self, context: Dict[Term, Tensor], map_default_to_term=False): 13 | self._context = context 14 | self._hash = hash(tuple(sorted(self._context.items()))) 15 | self._map_default_to_term = map_default_to_term 16 | 17 | def has_tensor_representation(self, term: Term) -> bool: 18 | return term in self._context 19 | 20 | def get_tensor_representation(self, term: Term) -> Union[Tensor, str]: 21 | """ 22 | Returns the tensor representation, unless it doesn't contain it, then it turns just the functor 23 | """ 24 | if self._map_default_to_term and not self.has_tensor_representation(term): 25 | return term.functor 26 | if term.is_list(): 27 | return torch.cat( 28 | [self.get_tensor_representation(a) for a in term.arguments] 29 | ) 30 | 31 | return self._context[term] 32 | 33 | def get_all_tensor_representations(self, network_input_args) -> List[Tensor]: 34 | return [self.get_tensor_representation(term) for term in network_input_args] 35 | 36 | def __eq__(self, other): 37 | if isinstance(other, Context): 38 | return self._context == other._context 39 | return False 40 | 41 | def __hash__(self): 42 | return self._hash 43 | 44 | 45 | class ContextualizedTerm: 46 | def __init__( 47 | self, context: Context, term: Term, probability: float = 1.0, meta=None 48 | ): 49 | self.context = context 50 | self.term = term 51 | self.probability = probability 52 | self.meta = meta 53 | 54 | def __str__(self): 55 | return "ContextualizedTerm(" + str(self.term) + ")" 56 | 57 | def __repr__(self): 58 | return str(self) 59 | 60 | def __eq__(self, other): 61 | if isinstance(other, ContextualizedTerm): 62 | return self.context == other.context and self.term == other.term 63 | return False 64 | 65 | def __hash__(self): 66 | return hash((self.context, self.term)) 67 | 68 | def mask_generation_output(self): 69 | return ContextualizedTerm( 70 | term=self.term.mask_generation_output(), 71 | context=self.context, 72 | ) 73 | -------------------------------------------------------------------------------- /deepstochlog/dataloader.py: -------------------------------------------------------------------------------- 1 | import random 2 | from math import ceil 3 | from typing import Sequence, List 4 | 5 | 6 | class DataLoader(object): 7 | def __init__( 8 | self, 9 | dataset: Sequence[any], 10 | batch_size: int, 11 | shuffle: bool = True, 12 | max_size: int = None, 13 | ): 14 | self.dataset = dataset 15 | self.batch_size = batch_size 16 | self.shuffle = shuffle 17 | if max_size is not None: 18 | self.length = min(self.length, max_size) 19 | else: 20 | self.length = len(dataset) 21 | 22 | self.dataset = list(self.dataset[: self.length]) 23 | 24 | # Set up the iterator 25 | self.idx_iterator = None 26 | self._create_new_iterator() 27 | 28 | def _create_new_iterator(self): 29 | idxs = list(range(self.length)) 30 | if self.shuffle: 31 | random.shuffle(idxs) 32 | self.idx_iterator = iter(idxs) 33 | 34 | def __next__(self) -> List: 35 | if self.idx_iterator is None: 36 | self._create_new_iterator() 37 | raise StopIteration 38 | batch = list() 39 | try: 40 | for _ in range(self.batch_size): 41 | batch.append(self.dataset[next(self.idx_iterator)]) 42 | except StopIteration: 43 | if len(batch) == 0: 44 | self._create_new_iterator() 45 | raise StopIteration 46 | else: 47 | self.idx_iterator = None 48 | return batch 49 | 50 | def __iter__(self): 51 | return self 52 | 53 | def __len__(self): 54 | return int(ceil(self.length / self.batch_size)) 55 | 56 | def __repr__(self): 57 | return "DataLoader: " + str(self.dataset[0]) 58 | -------------------------------------------------------------------------------- /deepstochlog/dataset.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from collections.abc import Sequence 3 | from typing import Set 4 | 5 | from deepstochlog.context import ContextualizedTerm 6 | from deepstochlog.term import Term, List 7 | 8 | 9 | class ContextualizedTermDataset(Sequence, ABC): 10 | def calculate_queries( 11 | self, masked_generation_output=False, limit=None 12 | ) -> Set[Term]: 13 | """ Calculates which queries are necessary to ask Prolog based on which terms are in the dataset """ 14 | queries = set() 15 | max_len = limit if limit is not None else len(self) 16 | for idx in range(max_len): 17 | elem: ContextualizedTerm = self[idx] 18 | if masked_generation_output: 19 | query = elem.term.mask_generation_output() 20 | else: 21 | query = elem.term 22 | if query not in queries: 23 | queries.add(query) 24 | return queries 25 | -------------------------------------------------------------------------------- /deepstochlog/inferences.py: -------------------------------------------------------------------------------- 1 | # These classes describe visitors that change the way the and/or trees are inferenced 2 | import abc 3 | import itertools 4 | from functools import reduce 5 | from typing import Tuple, Iterable, Callable 6 | 7 | import torch 8 | 9 | from deepstochlog.context import Context 10 | from deepstochlog.logic import ( 11 | And, 12 | Or, 13 | NNLeaf, 14 | TermLeaf, 15 | AbstractStaticProbability, 16 | LogicNode, 17 | ListBasedNode, 18 | ) 19 | from deepstochlog.term import Term 20 | 21 | 22 | def multiply(x, y): 23 | return x * y 24 | 25 | 26 | def multiply_iterable(iterable): 27 | return reduce(multiply, iterable) 28 | 29 | 30 | class LogicNodeVisitor(abc.ABC): 31 | def visit_and(self, node: And): 32 | raise NotImplementedError() 33 | 34 | def visit_or(self, node: Or): 35 | raise NotImplementedError() 36 | 37 | def visit_nnleaf(self, node: NNLeaf): 38 | raise NotImplementedError() 39 | 40 | def visit_termleaf(self, node: TermLeaf): 41 | raise NotImplementedError() 42 | 43 | def visit_static_probability(self, node: AbstractStaticProbability): 44 | raise NotImplementedError() 45 | 46 | 47 | # == SEMIRING BASED VISITORS == 48 | 49 | 50 | class SemiringVisitor(LogicNodeVisitor, abc.ABC): 51 | def __init__( 52 | self, probability_evaluator: "LogicProbabilityEvaluator", context: Context 53 | ): 54 | self.probability_evaluator = probability_evaluator 55 | self.context = context 56 | 57 | def visit_nnleaf(self, node: NNLeaf): 58 | return self.probability_evaluator.evaluate_neural_network_probability( 59 | node.network, node.inputs, node.index, self.context 60 | ) 61 | 62 | def visit_termleaf(self, node: TermLeaf): 63 | return self.probability_evaluator.accept_term_visitor(node.term, visitor=self) 64 | 65 | def visit_static_probability(self, node: AbstractStaticProbability): 66 | return torch.as_tensor( 67 | node.probability, device=self.probability_evaluator.device 68 | ) 69 | 70 | 71 | class SumProductVisitor(SemiringVisitor): 72 | def __init__( 73 | self, probability_evaluator: "LogicProbabilityEvaluator", context: Context 74 | ): 75 | super().__init__(probability_evaluator, context) 76 | 77 | def visit_and(self, node: And): 78 | return multiply_iterable((x.accept_visitor(self) for x in node.children)) 79 | 80 | def visit_or(self, node: Or): 81 | return sum(x.accept_visitor(self) for x in node.children) 82 | 83 | 84 | class MaxProductVisitor(SemiringVisitor): 85 | def __init__( 86 | self, probability_evaluator: "LogicProbabilityEvaluator", context: Context 87 | ): 88 | super().__init__(probability_evaluator, context) 89 | 90 | def get_children_max_product_probability_and_parse( 91 | self, node: ListBasedNode 92 | ) -> Tuple[Iterable[torch.Tensor], Iterable[Iterable[LogicNode]]]: 93 | values: Iterable[Tuple[torch.Tensor, Iterable[LogicNode]]] = ( 94 | x.accept_visitor(self) for x in node.children 95 | ) 96 | zipped: Tuple[Iterable[torch.Tensor], Iterable[Iterable[LogicNode]]] = zip( 97 | *values 98 | ) 99 | return zipped 100 | 101 | def visit_and(self, node: And) -> Tuple[torch.Tensor, Iterable["LogicNode"]]: 102 | tensors, logic_nodes = self.get_children_max_product_probability_and_parse(node) 103 | return multiply_iterable(tensors), ( 104 | x for sublist in logic_nodes for x in sublist 105 | ) 106 | 107 | def visit_or(self, node: Or) -> Tuple[torch.Tensor, Iterable["LogicNode"]]: 108 | tensors, logic_nodes = self.get_children_max_product_probability_and_parse(node) 109 | tensors = list(tensors) 110 | logic_nodes = list(logic_nodes) 111 | max_idx = torch.argmax(torch.stack(tensors)) 112 | 113 | return tensors[max_idx], logic_nodes[max_idx] 114 | 115 | def visit_nnleaf(self, node: NNLeaf) -> Tuple[torch.Tensor, Iterable["LogicNode"]]: 116 | return ( 117 | super().visit_nnleaf(node), 118 | [node], 119 | ) 120 | 121 | def visit_termleaf( 122 | self, node: TermLeaf 123 | ) -> Tuple[torch.Tensor, Iterable[LogicNode]]: 124 | prob, logic_nodes = super().visit_termleaf(node) 125 | return prob, itertools.chain([node], logic_nodes) 126 | 127 | def visit_static_probability(self, node: AbstractStaticProbability): 128 | return ( 129 | super().visit_static_probability(node), 130 | [node], 131 | ) 132 | 133 | 134 | # == LEAF DESCENDANTS INSPECTORS == 135 | 136 | 137 | class NNLeafDescendantsRetriever(LogicNodeVisitor): 138 | def visit_list_based(self, node: ListBasedNode) -> Iterable["NNLeaf"]: 139 | neural_leaf_descendents = [c.accept_visitor(self) for c in node.children] 140 | # Flatmap 141 | return [item for descendents in neural_leaf_descendents for item in descendents] 142 | 143 | def visit_and(self, node: And) -> Iterable["NNLeaf"]: 144 | return self.visit_list_based(node) 145 | 146 | def visit_or(self, node: Or) -> Iterable["NNLeaf"]: 147 | return self.visit_list_based(node) 148 | 149 | def visit_nnleaf(self, node: NNLeaf) -> Iterable["NNLeaf"]: 150 | return (node,) 151 | 152 | def visit_termleaf(self, node: TermLeaf) -> Iterable["NNLeaf"]: 153 | return () 154 | 155 | def visit_static_probability( 156 | self, node: AbstractStaticProbability 157 | ) -> Iterable["NNLeaf"]: 158 | return [] 159 | 160 | 161 | class TermLeafDescendantsRetriever(LogicNodeVisitor): 162 | def visit_list_based(self, node: ListBasedNode) -> Iterable["TermLeaf"]: 163 | term_leaf_descendants = [c.accept_visitor(self) for c in node.children] 164 | # Flatmap 165 | return [item for descendents in term_leaf_descendants for item in descendents] 166 | 167 | def visit_and(self, node: And) -> Iterable["TermLeaf"]: 168 | return self.visit_list_based(node) 169 | 170 | def visit_or(self, node: Or) -> Iterable["TermLeaf"]: 171 | return self.visit_list_based(node) 172 | 173 | def visit_nnleaf(self, node: NNLeaf) -> Iterable["TermLeaf"]: 174 | return () 175 | 176 | def visit_termleaf(self, node: TermLeaf) -> Iterable["TermLeaf"]: 177 | return (node,) 178 | 179 | def visit_static_probability( 180 | self, node: AbstractStaticProbability 181 | ) -> Iterable["TermLeaf"]: 182 | return [] 183 | 184 | 185 | class DescendantTermMapper(LogicNodeVisitor): 186 | def __init__(self, mapper: Callable[[Term], Term]): 187 | self.mapper = mapper 188 | 189 | def visit_list_based(self, node: ListBasedNode, logic_node_class) -> "LogicNode": 190 | return logic_node_class( 191 | *[child.accept_visitor(self) for child in node.children] 192 | ) 193 | 194 | def visit_and(self, node: And) -> "LogicNode": 195 | return self.visit_list_based(node, And) 196 | 197 | def visit_or(self, node: Or) -> "LogicNode": 198 | return self.visit_list_based(node, Or) 199 | 200 | def visit_nnleaf(self, node: NNLeaf) -> "LogicNode": 201 | return node 202 | 203 | def visit_termleaf(self, node: TermLeaf) -> "LogicNode": 204 | return TermLeaf(self.mapper(node.term)) 205 | 206 | def visit_static_probability(self, node: AbstractStaticProbability) -> "LogicNode": 207 | return node 208 | 209 | def accept_raw_nn_leaf(self, node: "RawNNLeaf"): 210 | 211 | new_nn_term = self.mapper(node.term) 212 | network_name = new_nn_term.arguments[0].functor 213 | index = node.networks.get_network(network_name).term2idx( 214 | new_nn_term.arguments[1] 215 | ) 216 | 217 | result = NNLeaf( 218 | network_name, 219 | index, 220 | new_nn_term.arguments[2], 221 | ) 222 | return result 223 | 224 | 225 | class SortedPrinter(LogicNodeVisitor): 226 | def visit_list_based(self, node: ListBasedNode, logic_node_class) -> str: 227 | children = [child.accept_visitor(self) for child in node.children] 228 | children.sort() 229 | return str(logic_node_class) + "(" + ", ".join(children) + ")" 230 | 231 | def visit_and(self, node: And) -> str: 232 | return self.visit_list_based(node, And) 233 | 234 | def visit_or(self, node: Or) -> str: 235 | return self.visit_list_based(node, Or) 236 | 237 | def visit_nnleaf(self, node: NNLeaf) -> str: 238 | return str(node) 239 | 240 | def visit_termleaf(self, node: TermLeaf) -> str: 241 | return str(node) 242 | 243 | def visit_static_probability(self, node: AbstractStaticProbability) -> str: 244 | return str(node) 245 | -------------------------------------------------------------------------------- /deepstochlog/logic.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, ABC 2 | from collections import Sized 3 | from typing import Callable, Iterable 4 | from deepstochlog.networkevaluation import RequiredEvaluation 5 | from deepstochlog.term import Term 6 | 7 | 8 | class LogicNode(Sized): 9 | def accept_visitor(self, visitor: "LogicNodeVisitor"): 10 | raise NotImplementedError() 11 | 12 | def name(self): 13 | return type(self).__name__ 14 | 15 | def __len__(self) -> int: 16 | return 1 17 | 18 | 19 | class ListBasedNode(LogicNode, metaclass=ABCMeta): 20 | def __init__(self, *args): 21 | super().__init__() 22 | self.children = frozenset(args) 23 | 24 | def add_children(self, *arguments): 25 | self.children |= frozenset(self.children | set(arguments)) 26 | 27 | def _string_children(self): 28 | return ",".join(str(c) for c in self.children) 29 | 30 | def __len__(self): 31 | return len(self.children) 32 | 33 | def __eq__(self, other): 34 | """Overrides the default implementation""" 35 | if isinstance(other, type(self)): 36 | return self.children == other.children 37 | return False 38 | 39 | def __hash__(self): 40 | """Overrides the default implementation""" 41 | return hash((type(self), self.children)) 42 | 43 | 44 | class And(ListBasedNode): 45 | def __init__(self, *args): 46 | super().__init__(*args) 47 | 48 | def accept_visitor(self, visitor: "LogicNodeVisitor"): 49 | return visitor.visit_and(self) 50 | 51 | def __str__(self): 52 | return "And(" + self._string_children() + ")" 53 | 54 | def __repr__(self): 55 | return str(self) 56 | 57 | 58 | class Or(ListBasedNode): 59 | def __init__(self, *args): 60 | super().__init__(*args) 61 | 62 | def accept_visitor(self, visitor: "LogicNodeVisitor"): 63 | return visitor.visit_or(self) 64 | 65 | def __str__(self): 66 | return "Or(" + self._string_children() + ")" 67 | 68 | def __repr__(self): 69 | return str(self) 70 | 71 | 72 | class NNLeaf(LogicNode): 73 | """ 74 | Logic leaf node for calculating the probability using a neural network model 75 | """ 76 | 77 | def __init__(self, network_model: str, index: int, inputs: Iterable): 78 | self.network = network_model 79 | self.index = index 80 | self.inputs = tuple(inputs) 81 | super().__init__() 82 | 83 | def accept_visitor(self, visitor: "LogicNodeVisitor"): 84 | return visitor.visit_nnleaf(self) 85 | 86 | def __str__(self): 87 | return ( 88 | "NNLeaf(" 89 | + str(self.network) 90 | + "," 91 | + str(self.index) 92 | + "," 93 | + str(self.inputs) 94 | + ")" 95 | ) 96 | 97 | def __repr__(self): 98 | return str(self) 99 | 100 | def name(self): 101 | return str((self.network, self.index, self.inputs)) 102 | 103 | def __len__(self): 104 | return 1 105 | 106 | def __eq__(self, other): 107 | """Overrides the default implementation""" 108 | if isinstance(other, NNLeaf): 109 | return (self.network, self.index, self.inputs) == ( 110 | other.network, 111 | other.index, 112 | other.inputs, 113 | ) 114 | return False 115 | 116 | def __hash__(self): 117 | """Overrides the default implementation""" 118 | return hash((self.network, self.index, self.inputs)) 119 | 120 | def to_required_evaluation(self, context): 121 | return RequiredEvaluation( 122 | context=context, network_name=self.network, input_args=self.inputs 123 | ) 124 | 125 | 126 | class TermLeaf(LogicNode): 127 | """ 128 | Term at the end of a logic node. 129 | Used for tabled logic trees 130 | """ 131 | 132 | def __init__(self, term: Term): 133 | super().__init__() 134 | self.term = term 135 | 136 | def accept_visitor(self, visitor: "LogicNodeVisitor"): 137 | return visitor.visit_termleaf(self) 138 | 139 | def __str__(self): 140 | return "TermLeaf(" + str(self.term) + ")" 141 | 142 | def __repr__(self): 143 | return str(self) 144 | 145 | def __eq__(self, other): 146 | """Overrides the default implementation""" 147 | if isinstance(other, TermLeaf): 148 | return self.term == other.term 149 | return False 150 | 151 | def __hash__(self): 152 | """Overrides the default implementation""" 153 | return hash(self.term) 154 | 155 | 156 | class AbstractStaticProbability(LogicNode, ABC): 157 | def __init__(self, probability: float): 158 | self.probability = probability 159 | 160 | def accept_visitor(self, visitor: "LogicNodeVisitor"): 161 | return visitor.visit_static_probability(self) 162 | 163 | def __repr__(self): 164 | return str(self) 165 | 166 | 167 | class StaticProbability(AbstractStaticProbability): 168 | def __init__(self, probability: float): 169 | super().__init__(probability) 170 | 171 | def __str__(self): 172 | return "StaticProbability(" + str(self.probability) + ")" 173 | 174 | def __eq__(self, other): 175 | """Overrides the default implementation""" 176 | return ( 177 | isinstance(other, StaticProbability) 178 | and self.probability == other.probability 179 | ) 180 | 181 | def __hash__(self): 182 | """Overrides the default implementation""" 183 | return 41 + hash(self.probability) 184 | 185 | 186 | class AlwaysTrue(AbstractStaticProbability): 187 | def __init__(self): 188 | super().__init__(1.0) 189 | 190 | def __str__(self): 191 | return "AlwaysTrue()" 192 | 193 | def __eq__(self, other): 194 | """Overrides the default implementation""" 195 | return isinstance(other, AlwaysTrue) 196 | 197 | def __hash__(self): 198 | """Overrides the default implementation""" 199 | return 123 200 | 201 | 202 | class AlwaysFalse(AbstractStaticProbability): 203 | def __init__(self): 204 | super().__init__(0.0) 205 | 206 | def __str__(self): 207 | return "AlwaysFalse()" 208 | 209 | def __eq__(self, other): 210 | """Overrides the default implementation""" 211 | return isinstance(other, AlwaysFalse) 212 | 213 | def __hash__(self): 214 | """Overrides the default implementation""" 215 | return 321 216 | -------------------------------------------------------------------------------- /deepstochlog/network.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import torch.nn as nn 4 | 5 | from deepstochlog.term import Term 6 | 7 | 8 | class Network(object): 9 | def __init__( 10 | self, 11 | name: str, 12 | neural_model: nn.Module, 13 | index_list: List[Term], 14 | concat_tensor_input=True, 15 | ): 16 | self.name = name 17 | self.neural_model = neural_model 18 | self.computation_graphs = dict() 19 | self.index_list = index_list 20 | self.index_mapping = dict() 21 | if index_list is not None: 22 | for i, elem in enumerate(index_list): 23 | self.index_mapping[elem] = i 24 | self.concat_tensor_input = concat_tensor_input 25 | 26 | def term2idx(self, term: Term) -> int: 27 | #TODO(giuseppe) index only with the functor 28 | 29 | # key = term #old 30 | key = Term(str(term.functor)) 31 | if key not in self.index_mapping: 32 | raise Exception( 33 | "Index was not found, did you include the right Term list as keys? Error item: " 34 | + str(term) 35 | + " " 36 | + str(type(term)) 37 | + ".\nPossible values: " 38 | + ", ".join([str(k) for k in self.index_mapping.keys()]) 39 | ) 40 | return self.index_mapping[key] 41 | 42 | def idx2term(self, index: int) -> Term: 43 | return self.index_list[index] 44 | 45 | def to(self, *args, **kwargs): 46 | self.neural_model.to(*args, **kwargs) 47 | 48 | 49 | class NetworkStore: 50 | def __init__(self, *networks: Network): 51 | self.networks = dict() 52 | for n in networks: 53 | self.networks[n.name] = n 54 | 55 | def get_network(self, name: str) -> Network: 56 | return self.networks[name] 57 | 58 | def to_device(self, *args, **kwargs): 59 | for network in self.networks.values(): 60 | network.to(*args, **kwargs) 61 | 62 | def get_all_net_parameters(self): 63 | all_parameters = list() 64 | for network in self.networks.values(): 65 | all_parameters.extend(network.neural_model.parameters()) 66 | return all_parameters 67 | 68 | def __add__(self, other: "NetworkStore"): 69 | return NetworkStore( 70 | *(list(self.networks.values()) + list(other.networks.values())) 71 | ) 72 | -------------------------------------------------------------------------------- /deepstochlog/networkevaluation.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from typing import Dict, Tuple, Iterable, List 3 | 4 | import torch 5 | from torch import Tensor 6 | 7 | from deepstochlog.context import Context 8 | from deepstochlog.network import NetworkStore 9 | 10 | 11 | class RequiredEvaluation: 12 | def __init__(self, context: Context, network_name: str, input_args: Tuple): 13 | self.context = context 14 | self.network_name = network_name 15 | self.input_args = input_args 16 | 17 | def prepare(self) -> "PreparedEvaluation": 18 | """ Prepares the evaluation by mapping the input variables to the right tensors """ 19 | mapped_input_args = self.context.get_all_tensor_representations(self.input_args) 20 | return PreparedEvaluation(self, mapped_input_args) 21 | 22 | def __str__(self): 23 | return ( 24 | "RequiredEvaluation(, " 25 | + self.network_name 26 | + ", " 27 | + str(self.input_args) 28 | + ")" 29 | ) 30 | 31 | def __repr__(self): 32 | return str(self) 33 | 34 | def __eq__(self, other): 35 | """Overrides the default implementation""" 36 | if isinstance(other, RequiredEvaluation): 37 | return ( 38 | self.context == other.context 39 | and self.network_name == other.network_name 40 | and self.input_args == other.input_args 41 | ) 42 | return False 43 | 44 | def __hash__(self): 45 | return hash((self.context, self.network_name, self.input_args)) 46 | 47 | 48 | class PreparedEvaluation: 49 | def __init__( 50 | self, required_evaluation: RequiredEvaluation, mapped_input_args: List[Tensor] 51 | ): 52 | self.required_evaluation = required_evaluation 53 | self.mapped_input_args = mapped_input_args 54 | 55 | def has_tensors(self): 56 | return len(self.mapped_input_args) > 0 57 | 58 | def __str__(self): 59 | return ( 60 | "PreparedEvaluation(" 61 | + str(self.required_evaluation) 62 | + ", tensor_list(len:" 63 | + str(len(self.mapped_input_args)) 64 | + "))" 65 | ) 66 | 67 | def __repr__(self): 68 | return str(self) 69 | 70 | 71 | def extract_input_arguments(prepared_evaluations: Iterable[PreparedEvaluation]): 72 | return [ 73 | torch.cat(pe.mapped_input_args, 0) if pe.has_tensors() else torch.tensor([]) 74 | for pe in prepared_evaluations 75 | ] 76 | 77 | 78 | class NetworkEvaluations: 79 | def __init__(self): 80 | self.evaluations: Dict[Context, Dict[str, Dict[Tuple, Tensor]]] = defaultdict( 81 | lambda: defaultdict(defaultdict) 82 | ) 83 | 84 | def add_evaluation_result( 85 | self, context: Context, network_name: str, input_args: Tuple, output: Tensor 86 | ): 87 | self.evaluations[context][network_name][input_args] = output 88 | 89 | def get_evaluation_result( 90 | self, context: Context, network_name: str, input_args: Tuple 91 | ) -> Tensor: 92 | # print(context, network_name, input_args) 93 | return self.evaluations[context][network_name][input_args] 94 | 95 | @staticmethod 96 | def from_required_evaluations( 97 | required_evaluations: Iterable[RequiredEvaluation], 98 | networks: NetworkStore, 99 | device=None, 100 | ): 101 | """ Evaluates all networks for a list of required evaluations """ 102 | # Group all required evaluations per network, and prepare required evaluation 103 | per_network: Dict[str, List[PreparedEvaluation]] = defaultdict(list) 104 | for req in required_evaluations: 105 | per_network[req.network_name].append(req.prepare()) 106 | 107 | # Evaluate on network 108 | network_evaluations: NetworkEvaluations = NetworkEvaluations() 109 | for network_name, prepared_evaluations in per_network.items(): 110 | # Convert to 111 | network = networks.get_network(network_name) 112 | 113 | if network.concat_tensor_input: 114 | all_to_evaluate: List[Tensor] = extract_input_arguments( 115 | prepared_evaluations 116 | ) 117 | 118 | # neural_input = torch.cat(all_to_evaluate, 0).unsqueeze(1) 119 | neural_input = torch.nn.utils.rnn.pad_sequence( 120 | all_to_evaluate, batch_first=True 121 | ) 122 | # neural_input = torch.stack(all_to_evaluate) 123 | 124 | if device: 125 | neural_input = neural_input.to(device) 126 | else: 127 | neural_input = [pe.mapped_input_args for pe in prepared_evaluations] 128 | 129 | outputs = network.neural_model(neural_input) 130 | 131 | # Store result 132 | required_evaluations = [ 133 | pe.required_evaluation for pe in prepared_evaluations 134 | ] 135 | for re, output in zip(required_evaluations, outputs): 136 | network_evaluations.add_evaluation_result( 137 | context=re.context, 138 | network_name=re.network_name, 139 | input_args=re.input_args, 140 | output=output, 141 | ) 142 | 143 | return network_evaluations 144 | -------------------------------------------------------------------------------- /deepstochlog/nn_models.py: -------------------------------------------------------------------------------- 1 | import torch 2 | 3 | 4 | class TrainableProbability(torch.nn.Module): 5 | def __init__(self, N): 6 | super().__init__() 7 | self.vars = torch.nn.Parameter(torch.ones([1, N])) 8 | 9 | def forward(self, X): 10 | shape = X.shape[0] 11 | p = torch.softmax(self.vars, dim=-1).squeeze(0) 12 | return [p for _ in range(shape)] 13 | -------------------------------------------------------------------------------- /deepstochlog/parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List 3 | 4 | from pyparsing import ( 5 | Word, 6 | alphas, 7 | nums, 8 | delimitedList, 9 | Suppress, 10 | Combine, 11 | Optional, 12 | OneOrMore, 13 | ParseResults, 14 | Group, 15 | Regex, 16 | Or, 17 | Forward, 18 | ParseException, 19 | ) 20 | 21 | from deepstochlog.rule import ( 22 | TrainableProbabilityAnnotation, 23 | VariableProbabilityAnnotation, 24 | StaticProbabilityAnnotation, 25 | NeuralProbabilityAnnotation, 26 | NDCGRule, 27 | ProbabilityAnnotation, 28 | Rule, 29 | Fact, 30 | ClauseRule, 31 | ProgramRules, 32 | ) 33 | from deepstochlog.term import Term 34 | 35 | 36 | def create_rules_parser(): 37 | static_probability = ( 38 | Combine(Optional("0") + "." + Word(nums)) 39 | | Combine("1" + Optional("." + OneOrMore("0"))) 40 | ).setResultsName("static_probability") 41 | 42 | variable_probability = Word(alphas.upper()).setResultsName("variable_probability") 43 | 44 | trainable_probability = Combine("t(_)").setResultsName("trainable_probability") 45 | 46 | any_alphanumeric = alphas + nums + "_" 47 | any_alphanumeric_word = Word(any_alphanumeric) 48 | variable = Word(alphas.upper() + "_", alphas + nums + "_") 49 | model_name = any_alphanumeric_word 50 | input_vars = ( 51 | Suppress("[") + Group(Optional(delimitedList(Group(variable)))) + Suppress("]") 52 | ) 53 | neural_probability = ( 54 | Suppress("nn(") 55 | + model_name.setResultsName("model_name") 56 | + Suppress(",") 57 | + input_vars.setResultsName("input_vars") 58 | + Suppress(",") 59 | + Group(variable).setResultsName("output_var") 60 | + Suppress(",") 61 | + any_alphanumeric_word.setResultsName("output_domain") 62 | # + Suppress("[") 63 | # + Group(delimitedList(any_alphanumeric_word)).setResultsName("output_domain") 64 | # + Suppress("]") 65 | + Suppress(")") 66 | ).setResultsName("neural_probability") 67 | 68 | any_probability = static_probability | variable_probability | trainable_probability | neural_probability 69 | 70 | term_forward = Forward() 71 | term = any_alphanumeric_word + Optional( 72 | Suppress("(") 73 | + Group(delimitedList(Group(term_forward)).setResultsName("arguments")) 74 | + Suppress(")") 75 | ) 76 | term_forward << term 77 | 78 | # clause = ( 79 | # term 80 | # ^ (term + ";" + term) 81 | # ^ (term + "," + term) 82 | # ^ ( 83 | # term 84 | # + Or(["=", "is"]) 85 | # + term 86 | # + Or(["+", "-", "*", "/", "**", "%"]) 87 | # + term 88 | # ) 89 | # ^ (term + Or(["<", ">", "=<", ">="]) + term) 90 | # ^ ("{" + term + "}") 91 | # ) 92 | # body = clause 93 | 94 | # Disallow --> and :- to occur in body, as this usually indicates a bug, e.g. forgotten period 95 | # Allow quoted dots, dots followed by a digit, and otherwise any character that isn't a dot 96 | body = Regex( 97 | r"((?!([-][-][>])|([:][-]))(([^\\][\"]([^\"])*[^\\][\"])|(.\d)|[^.]))*" 98 | ) 99 | 100 | ndcg_rule = ( 101 | Optional( 102 | Group(any_probability.setResultsName("probability")) + Suppress("::") 103 | ).setResultsName("probability_annotation") 104 | + Group(term.setResultsName("head")) 105 | + Suppress("-->") 106 | + Group(body.setResultsName("body")) 107 | + Suppress(".") 108 | ).setResultsName("ndcg_rule") 109 | 110 | fact = (Group(term.setResultsName("head")) + Suppress(".")).setResultsName("fact") 111 | clause_rule = ( 112 | Group(term.setResultsName("head")) 113 | + Suppress(":-") 114 | + Group(body.setResultsName("body")) 115 | + Suppress(".") 116 | ).setResultsName("clause_rule") 117 | 118 | rule = ndcg_rule | clause_rule | fact 119 | 120 | rules = OneOrMore(Group(rule)).setResultsName("rules") 121 | return rules 122 | 123 | 124 | rules_parser = create_rules_parser() 125 | 126 | 127 | def parse_term(input_term: ParseResults) -> Term: 128 | functor = input_term[0] 129 | arguments = [] if len(input_term) == 1 else [parse_term(t) for t in input_term[1]] 130 | return Term(functor, *arguments) 131 | 132 | 133 | def parse_probability(p_probability) -> ProbabilityAnnotation: 134 | if len(p_probability) == 1: 135 | el = p_probability[0] 136 | if str(el).startswith("t"): 137 | return TrainableProbabilityAnnotation() 138 | if str(el)[0].isupper(): 139 | return VariableProbabilityAnnotation(el) 140 | else: 141 | return StaticProbabilityAnnotation(float(el)) 142 | else: 143 | model, p_input_vars, output_var, p_output_domain = p_probability 144 | input_vars = [parse_term(t) for t in p_input_vars] 145 | # output_domain = [parse_term(t) for t in p_output_domain] 146 | return NeuralProbabilityAnnotation( 147 | str(model), input_vars, parse_term(output_var), p_output_domain 148 | ) 149 | 150 | 151 | def process_body(p_body: ParseResults): 152 | """ Joins the elements of the body together, and removes unnecessary extra whitespace""" 153 | return re.sub(r"\s+", " ", "".join([str(el) for el in p_body])) 154 | 155 | 156 | def parse_rule(input_rule: ParseResults) -> Rule: 157 | # print("rule", input_rule) 158 | if "fact" in input_rule: 159 | (p_head,) = input_rule 160 | return Fact(parse_term(p_head)) 161 | if "clause_rule" in input_rule: 162 | p_head, p_body = input_rule 163 | return ClauseRule(parse_term(p_head), process_body(p_body)) 164 | if "ndcg_rule" in input_rule: 165 | if "probability_annotation" in input_rule: 166 | p_probability, p_head, p_body = input_rule 167 | probability = parse_probability(p_probability) 168 | else: 169 | # If no probability given, make it 1 170 | p_head, p_body = input_rule 171 | probability = StaticProbabilityAnnotation(1) 172 | 173 | return NDCGRule( 174 | probability=probability, 175 | head=parse_term(p_head), 176 | body=process_body(p_body), 177 | ) 178 | 179 | raise NotImplementedError("Unsupported rule type: " + str(input_rule)) 180 | 181 | 182 | def parse_rules(rules_str: str) -> ProgramRules: 183 | try: 184 | parsed_rules = rules_parser.parseString(rules_str, parseAll=True) 185 | except ParseException as error: 186 | # Add common mistake warning 187 | if 'Expected {{[{Group:({{Combine:({["0"]' in error.msg: 188 | error.msg += ( 189 | "\nDid you forget putting a closing period at the end of a rule?" 190 | "Check all lines above the given line number" 191 | ) 192 | raise error 193 | 194 | resulting_rules = [] 195 | 196 | for parsed_rule in parsed_rules: 197 | resulting_rules.append(parse_rule(parsed_rule)) 198 | 199 | print("Parsed rules:", "\n".join([str(r) for r in resulting_rules]), "\n", sep="\n") 200 | 201 | return ProgramRules(resulting_rules) 202 | -------------------------------------------------------------------------------- /deepstochlog/rule.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from collections import defaultdict 3 | from typing import Dict 4 | 5 | from deepstochlog.term import Term, List 6 | from deepstochlog.network import Network, NetworkStore 7 | from deepstochlog.nn_models import TrainableProbability 8 | 9 | 10 | class ProbabilityAnnotation: 11 | pass 12 | 13 | 14 | 15 | class VariableProbabilityAnnotation(ProbabilityAnnotation): 16 | def __init__(self, variable: str): 17 | self.variable = variable 18 | 19 | def __str__(self): 20 | return str(self.variable) 21 | 22 | def __repr__(self): 23 | return str(self) 24 | 25 | 26 | class StaticProbabilityAnnotation(ProbabilityAnnotation): 27 | def __init__(self, probability: float): 28 | self.probability = probability 29 | 30 | def __str__(self): 31 | return str(self.probability) 32 | 33 | def __repr__(self): 34 | return str(self) 35 | 36 | 37 | class TrainableProbabilityAnnotation(ProbabilityAnnotation): 38 | def __init__(self): 39 | pass 40 | 41 | def __str__(self): 42 | return "t(_)" 43 | 44 | def __repr__(self): 45 | return str(self) 46 | 47 | 48 | class NeuralProbabilityAnnotation(ProbabilityAnnotation): 49 | def __init__( 50 | self, 51 | model_name: str, 52 | input_var: typing.List[Term], 53 | output_var: Term, 54 | # output_domain: List[Term], 55 | output_domain: str, 56 | ): 57 | self.model_name = model_name 58 | self.input_var = input_var 59 | self.output_var = output_var 60 | self.output_domain = output_domain 61 | 62 | def __str__(self): 63 | return ( 64 | "nn(" 65 | + str(self.model_name) 66 | + ", " 67 | + str(self.input_var) 68 | + ", " 69 | + str(self.output_var) 70 | + ", " 71 | + str(self.output_domain) 72 | + ")" 73 | ) 74 | 75 | def __repr__(self): 76 | return str(self) 77 | 78 | 79 | class Rule: 80 | def __init__(self, head: Term): 81 | self.head = head 82 | 83 | 84 | class Fact(Rule): 85 | def __init__(self, head: Term): 86 | super().__init__(head) 87 | 88 | def __str__(self): 89 | return str(self.head) + "." 90 | 91 | def __repr__(self): 92 | return str(self) 93 | 94 | 95 | class ClauseRule(Rule): 96 | def __init__(self, head: Term, body: str): 97 | super().__init__(head) 98 | self.body = body 99 | 100 | def __str__(self): 101 | return str(self.head) + " :- " + self.body + "." 102 | 103 | def __repr__(self): 104 | return str(self) 105 | 106 | 107 | class NDCGRule(Rule): 108 | def __init__(self, probability: ProbabilityAnnotation, head: Term, body: str): 109 | super().__init__(head) 110 | self.probability = probability 111 | self.body = body 112 | 113 | def __str__(self): 114 | return ( 115 | str(self.probability) + " :: " + str(self.head) + " --> " + self.body + "." 116 | ) 117 | 118 | def __repr__(self): 119 | return str(self) 120 | 121 | 122 | def check_trainable_probability_support( 123 | trainable_probability_rules: typing.List[NDCGRule], rules: typing.List[NDCGRule] 124 | ): 125 | if len(trainable_probability_rules) > 0: 126 | # Check if all rules are trainable if at least one of them is. Otherwise, not (yet) supported 127 | if len(trainable_probability_rules) != len(rules): 128 | raise RuntimeError( 129 | "Rules need to either have all trainable probabilities" 130 | "or none. Mix of probability types in rules: {}".format( 131 | trainable_probability_rules 132 | ) 133 | ) 134 | # Checks if they all have the same arguments 135 | arguments = trainable_probability_rules[0].head.arguments 136 | for trainable_rule in trainable_probability_rules: 137 | if trainable_rule.head.arguments != arguments: 138 | raise RuntimeError( 139 | "Trainable probability rules all need to have the same exact arguments & arguments names." 140 | "Different arguments are not yet supported.\n" 141 | "Conflicting rules:\n{}\n{}".format( 142 | trainable_probability_rules[0], trainable_rule 143 | ) 144 | ) 145 | 146 | 147 | class ProgramRules: 148 | def __init__(self, rules: typing.List[Rule]): 149 | self.rules = rules 150 | 151 | # Divide over types of rules 152 | def get_ndcg_rules(self) -> typing.List[NDCGRule]: 153 | return [rule for rule in self.rules if isinstance(rule, NDCGRule)] 154 | 155 | def get_prolog_rules(self): 156 | return [ 157 | rule 158 | for rule in self.rules 159 | if isinstance(rule, ClauseRule) or isinstance(rule, Fact) 160 | ] 161 | 162 | # Properties checking 163 | def has_trainable_probabilities(self): 164 | return any( 165 | rule 166 | for rule in self.get_ndcg_rules() 167 | if isinstance(rule.probability, TrainableProbabilityAnnotation) 168 | ) 169 | 170 | # Transformations 171 | def remove_syntactic_sugar(self) -> typing.Tuple["ProgramRules", NetworkStore]: 172 | return self._transform_trainable_probabilities_to_switches() 173 | 174 | def _transform_trainable_probabilities_to_switches( 175 | self, 176 | ) -> typing.Tuple["ProgramRules", NetworkStore]: 177 | # Rules of the final program 178 | resulting_rules = [] 179 | new_networks = [] 180 | 181 | # Sort rules 182 | rules_per_head: Dict[ 183 | typing.Tuple[str, int], typing.List[NDCGRule] 184 | ] = defaultdict(list) 185 | for rule in self.rules: 186 | if isinstance(rule, NDCGRule): 187 | functor_arity = rule.head.get_functor_and_arity() 188 | rules_per_head[functor_arity].append(rule) 189 | else: 190 | resulting_rules.append(rule) 191 | 192 | # Create switch for each functor&arity if there are trainable parameters 193 | for ((functor, arity), rules) in rules_per_head.items(): 194 | trainable_probability_rules: typing.List[NDCGRule] = [ 195 | rule 196 | for rule in rules 197 | if isinstance(rule.probability, TrainableProbabilityAnnotation) 198 | ] 199 | 200 | # Check if there are trainable parameters 201 | if len(trainable_probability_rules) == 0: 202 | resulting_rules.extend(rules) 203 | else: 204 | check_trainable_probability_support(trainable_probability_rules, rules) 205 | 206 | # Create new predicates/heads/names 207 | switch_functor_name = "switch_{}_{}".format(functor, arity) 208 | dom_head_functor = "domain_{}".format(switch_functor_name) 209 | neural_network_name = "nn_{}".format(switch_functor_name) 210 | number_of_switch_rules = len(trainable_probability_rules) 211 | 212 | # Create domain of the switch variable 213 | domain_variable = Term("X") 214 | domain_range = [Term(str(i)) for i in range(number_of_switch_rules)] 215 | switch_domain_rule = ClauseRule( 216 | head=Term(dom_head_functor, domain_variable), 217 | body=str(Term("member", domain_variable, List(*domain_range))), 218 | ) 219 | resulting_rules.append(switch_domain_rule) 220 | 221 | # Create trainable probability 222 | switch_nn = Network( 223 | neural_network_name, 224 | TrainableProbability(N=number_of_switch_rules), 225 | index_list=domain_range, 226 | ) 227 | new_networks.append(switch_nn) 228 | 229 | # Create a rule mapping the original head to the switch, e.g. 230 | # nn(s_nn, [], Y, s_switch_dom) :: s --> s_switch(Y) 231 | switch_rule_variable = Term("Y") 232 | passed_arguments = trainable_probability_rules[0].head.arguments 233 | switch_arguments = [switch_rule_variable] + list(passed_arguments) 234 | 235 | switch_entry_rule = NDCGRule( 236 | probability=NeuralProbabilityAnnotation( 237 | model_name=neural_network_name.format(functor, arity), 238 | input_var=[], 239 | output_var=switch_rule_variable, 240 | output_domain=dom_head_functor, 241 | ), 242 | head=Term(functor, *passed_arguments), 243 | body=str(Term(switch_functor_name, *switch_arguments)), 244 | ) 245 | switch_choice_rules = [ 246 | NDCGRule( 247 | probability=StaticProbabilityAnnotation(1), 248 | head=Term( 249 | switch_functor_name, 250 | *([switch_rule_idx] + list(passed_arguments)), 251 | ), 252 | body=trainable_probability_rules[i].body, 253 | ) 254 | for i, switch_rule_idx in enumerate(domain_range) 255 | ] 256 | 257 | resulting_rules.append(switch_entry_rule) 258 | resulting_rules.extend(switch_choice_rules) 259 | 260 | return ProgramRules(resulting_rules), NetworkStore(*new_networks) 261 | 262 | # Buildins 263 | def __str__(self): 264 | return "\n".join([str(rule) for rule in self.rules]) 265 | 266 | def __repr__(self): 267 | return str(self) 268 | -------------------------------------------------------------------------------- /deepstochlog/tabled_and_or_trees.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from functools import lru_cache 3 | from typing import Dict, Tuple, List, Callable, Iterable 4 | 5 | import torch 6 | from torch import Tensor 7 | 8 | from deepstochlog.context import Context, ContextualizedTerm 9 | from deepstochlog.networkevaluation import NetworkEvaluations, RequiredEvaluation 10 | from deepstochlog.term import Term 11 | from deepstochlog.inferences import NNLeafDescendantsRetriever, TermLeafDescendantsRetriever 12 | 13 | 14 | class TabledAndOrTrees: 15 | """ 16 | Represents the grounded and/or tree, used for calculating the probabilities easily 17 | """ 18 | 19 | def __init__( 20 | self, 21 | and_or_tree: Dict[Term, "LogicNode"], 22 | terms_grounder: Callable[[List[Term]], Dict[Term, "LogicNode"]] = None, 23 | ): 24 | self._and_or_tree = and_or_tree 25 | self._terms_grounder = terms_grounder 26 | 27 | def has_and_or_tree(self, term: Term): 28 | return term in self._and_or_tree 29 | 30 | def get_and_or_tree(self, term: Term): 31 | if not self.has_and_or_tree(term): 32 | self.ground_all([term]) 33 | return self._and_or_tree[term] 34 | 35 | def ground_all(self, terms: List[Term]): 36 | if self._terms_grounder is None: 37 | raise RuntimeError( 38 | "Did not contain a term grounder, so could not ground missing terms", 39 | [str(term) for term in terms], 40 | ) 41 | new_elements = self._terms_grounder(terms) 42 | self._and_or_tree.update(new_elements) 43 | 44 | def calculate_required_evaluations( 45 | self, contextualized_term: ContextualizedTerm 46 | ) -> List[RequiredEvaluation]: 47 | nn_leafs = list() 48 | handled_terms = set() 49 | term_queue = [contextualized_term.term] 50 | # Get all descendent nn_leafs 51 | while len(term_queue) > 0: 52 | t = term_queue.pop() 53 | if t not in handled_terms: 54 | handled_terms.add(t) 55 | tree = self.get_and_or_tree(t) 56 | # Extend with all neural leafs 57 | nn_leafs.extend( 58 | tree.accept_visitor(visitor=NNLeafDescendantsRetriever()) 59 | ) 60 | # Add all term leafs as terms to handle 61 | term_queue.extend( 62 | [ 63 | t.term 64 | for t in tree.accept_visitor( 65 | visitor=TermLeafDescendantsRetriever() 66 | ) 67 | ] 68 | ) 69 | 70 | # Turn every leaf into a required evaluation 71 | return [ 72 | nn_leaf.to_required_evaluation(contextualized_term.context) 73 | for nn_leaf in nn_leafs 74 | ] 75 | 76 | 77 | class LogicProbabilityEvaluator: 78 | def __init__( 79 | self, 80 | trees: TabledAndOrTrees, 81 | network_evaluations: NetworkEvaluations, 82 | device=None, 83 | ): 84 | self.device = device 85 | self.trees = trees 86 | self.network_evaluations = network_evaluations 87 | 88 | @lru_cache() 89 | def accept_term_visitor(self, term: Term, visitor: "LogicNodeVisitor"): 90 | return self.trees.get_and_or_tree(term).accept_visitor(visitor=visitor) 91 | 92 | def evaluate_neural_network_probability( 93 | self, 94 | network: str, 95 | input_arguments: Tuple, 96 | index: int, 97 | context: Context = None, 98 | ) -> Tensor: 99 | return self.network_evaluations.get_evaluation_result( 100 | context=context, network_name=network, input_args=input_arguments 101 | )[index] 102 | -------------------------------------------------------------------------------- /deepstochlog/term.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from collections.abc import Sequence 3 | 4 | 5 | class Term(object): 6 | def __init__(self, functor: str, *arguments: "Term"): 7 | self._functor = functor 8 | self._arguments = arguments 9 | self.arity = len(arguments) 10 | 11 | # Calculate the representation already: used for hashing and equality 12 | self._repr = str(self._functor) + ( 13 | "({})".format(",".join([str(x) for x in self.arguments])) 14 | if self.arguments is not None and len(self.arguments) > 0 15 | else "" 16 | ) 17 | 18 | self._hash = hash(self._repr) 19 | 20 | @property 21 | def functor(self) -> str: 22 | return self._functor 23 | 24 | @property 25 | def arguments(self) -> typing.Tuple["Term"]: 26 | return self._arguments 27 | 28 | def get_functor_and_arity(self) -> (str, int): 29 | return self._functor, len(self._arguments) 30 | 31 | def __repr__(self): 32 | return self._repr 33 | 34 | def __eq__(self, other: "Term"): 35 | return ( 36 | hash(self) == hash(other) 37 | and isinstance(other, Term) 38 | and self._repr == other._repr 39 | ) 40 | 41 | def __lt__(self, other): 42 | return self._repr < other._repr 43 | 44 | def __gt__(self, other): 45 | return self._repr > other._repr 46 | 47 | def __hash__(self): 48 | return self._hash 49 | 50 | def with_extra_arguments( 51 | self, *arguments: typing.Union["Term", str, int] 52 | ) -> "Term": 53 | return Term(self._functor, *(self.arguments + arguments)) 54 | 55 | def without_difference_list( 56 | self, long_list_idx: int = -2, difference_list_idx: int = -1 57 | ) -> "Term": 58 | 59 | if len(self._arguments) < 2: 60 | return self 61 | 62 | long_list: List["Term"] = self._arguments[long_list_idx] 63 | # Remove the last X elements of the list, with X being the length of the difference list 64 | new_list = List( 65 | *long_list.arguments[ 66 | 0 : len(long_list) - len(self._arguments[difference_list_idx]) 67 | ] 68 | ) 69 | 70 | # Create new arguments for the term by adding all terms in front and to the back or this term. 71 | new_arguments = list(self._arguments[0:long_list_idx]) 72 | new_arguments.append(new_list) 73 | if difference_list_idx != -1: 74 | new_arguments.extend(self._arguments[difference_list_idx:-1]) 75 | 76 | return Term(self._functor, *new_arguments) 77 | 78 | # DCG specific 79 | def get_generation_output(self) -> "Term": 80 | return self._arguments[0] 81 | 82 | def can_mask_generation_output(self): 83 | return len(self.arguments) > 1 84 | 85 | def mask_generation_output(self) -> "Term": 86 | """ Removes the first argument in favor of an underscore, to 'mask' the desired output """ 87 | if not self.can_mask_generation_output(): 88 | # No generation output 89 | return self 90 | return self.change_generation_output(wildcard_term) 91 | 92 | def change_generation_output(self, new_first_argument: "Term") -> "Term": 93 | new_arguments = list(self._arguments) 94 | new_arguments[0] = new_first_argument 95 | return Term(self.functor, *new_arguments) 96 | 97 | def covers(self, el: "Term"): 98 | return self.functor == el.functor and all( 99 | self_arg == wildcard_term or self_arg == other_arg 100 | for (self_arg, other_arg) in zip(self.arguments, el.arguments) 101 | ) 102 | 103 | def contains_mask(self): 104 | return any(arg == wildcard_term for arg in self.arguments) 105 | 106 | def to_dsl_input(self): 107 | """ Adds the right arguments to be used as dsl input """ 108 | return self.with_extra_arguments(List()) 109 | 110 | def is_list(self): 111 | return False 112 | 113 | 114 | wildcard_term = Term("_") 115 | 116 | 117 | class List(Term, Sequence): 118 | def __init__(self, *arguments: typing.Union[Term, str, int]): 119 | super(List, self).__init__(".", *arguments) 120 | 121 | def __str__(self): 122 | return "[{}]".format(",".join((str(x) for x in self._arguments))) 123 | 124 | def __repr__(self): 125 | return str(self) 126 | 127 | def __len__(self): 128 | return len(self.arguments) 129 | 130 | def __getitem__(self, i): 131 | return self.arguments.__getitem__(i) 132 | 133 | def is_list(self): 134 | return True 135 | -------------------------------------------------------------------------------- /deepstochlog/trainer.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from typing import Callable, Tuple, List, Union 3 | 4 | import pandas as pd 5 | import torch 6 | from pandas import DataFrame 7 | 8 | from deepstochlog.dataloader import DataLoader 9 | from deepstochlog.model import DeepStochLogModel 10 | 11 | 12 | class DeepStochLogLogger: 13 | def log_header(self, accuracy_tester_header): 14 | pass 15 | 16 | def log( 17 | self, 18 | epoch: int, 19 | batch_idx: int, 20 | total_loss: float, 21 | instances_since_last_log: int, 22 | accuracy_tester: str, 23 | ): 24 | raise NotImplementedError() 25 | 26 | def print( 27 | self, 28 | line: str, 29 | ): 30 | print(line) 31 | 32 | 33 | class PrintLogger(DeepStochLogLogger): 34 | def log_header(self, accuracy_tester_header): 35 | print("Epoch\tBatch\tLoss\t\t" + accuracy_tester_header) 36 | 37 | def log( 38 | self, 39 | epoch: int, 40 | batch_idx: int, 41 | total_loss: float, 42 | instances_since_last_log: int, 43 | accuracy_tester: str, 44 | ): 45 | print( 46 | "{:>5}\t{:>5}\t{:.5f}\t\t{}".format( 47 | epoch, 48 | batch_idx, 49 | float(total_loss) / instances_since_last_log, 50 | accuracy_tester, 51 | ) 52 | ) 53 | 54 | 55 | class PrintFileLogger(DeepStochLogLogger): 56 | def __init__(self, filepath): 57 | self.filepath = filepath 58 | 59 | def _write_and_print(self, line): 60 | with open(self.filepath, "a") as f: 61 | f.write(line) 62 | f.write("\n") 63 | print(line) 64 | 65 | def log_header(self, accuracy_tester_header): 66 | line = "Epoch\tBatch\tLoss\t\t" + accuracy_tester_header 67 | self._write_and_print(line) 68 | 69 | def print(self, line): 70 | self._write_and_print(line) 71 | 72 | def log( 73 | self, 74 | epoch: int, 75 | batch_idx: int, 76 | total_loss: float, 77 | instances_since_last_log: int, 78 | accuracy_tester: str, 79 | ): 80 | 81 | line = "{:>5}\t{:>5}\t{:.5f}\t\t{}".format( 82 | epoch, 83 | batch_idx, 84 | float(total_loss) / instances_since_last_log, 85 | accuracy_tester, 86 | ) 87 | self._write_and_print(line) 88 | 89 | 90 | class PandasLogger(DeepStochLogLogger): 91 | def __init__(self): 92 | self.df: DataFrame = DataFrame() 93 | 94 | def log_header(self, accuracy_tester_header): 95 | columns = ["Epoch", "Batch", "Loss"] + accuracy_tester_header.split("\t") 96 | self.df = DataFrame(data=[], columns=columns) 97 | 98 | def log( 99 | self, 100 | epoch: int, 101 | batch_idx: int, 102 | total_loss: float, 103 | instances_since_last_log: int, 104 | accuracy_tester: str, 105 | ): 106 | to_append: List[Union[str, float, int]] = [ 107 | epoch, 108 | batch_idx, 109 | total_loss / instances_since_last_log, 110 | ] 111 | to_append.extend( 112 | [float(el) for el in accuracy_tester.split("\t") if len(el.strip()) > 0] 113 | ) 114 | series = pd.Series(to_append, index=self.df.columns) 115 | self.df = self.df.append(series, ignore_index=True) 116 | 117 | def get_last_result(self): 118 | return self.df.iloc[[-1]] 119 | 120 | 121 | print_logger = PrintLogger() 122 | 123 | 124 | class DeepStochLogTrainer: 125 | def __init__( 126 | self, 127 | logger: DeepStochLogLogger = print_logger, 128 | log_freq: int = 50, 129 | accuracy_tester: Tuple[str, Callable[[], str]] = (), 130 | test_query=None, 131 | print_time=False, 132 | allow_zero_probability_examples=False, 133 | ): 134 | self.logger = logger 135 | self.log_freq = log_freq 136 | self.accuracy_tester_header, self.accuracy_tester_fn = accuracy_tester 137 | self.test_query = test_query 138 | self.print_time = print_time 139 | self.allow_zero_probability_examples = allow_zero_probability_examples 140 | 141 | def train( 142 | self, 143 | model: DeepStochLogModel, 144 | optimizer, 145 | dataloader: DataLoader, 146 | epochs: int, 147 | epsilon=1e-8, 148 | ): 149 | # Test the performance using the test query 150 | if self.test_query is not None: 151 | self.test_query() 152 | 153 | # Log time 154 | training_start = time() 155 | 156 | # Start training 157 | batch_idx = 0 158 | total_loss = 0 159 | instances_since_last_log = 0 160 | self.logger.log_header(self.accuracy_tester_header) 161 | for epoch in range(epochs): 162 | for batch in dataloader: 163 | 164 | # Cross-Entropy (CE) loss 165 | probabilities = model.predict_sum_product(batch) 166 | if self.allow_zero_probability_examples: 167 | targets = torch.as_tensor( 168 | [el.probability for el in batch], device=model.device 169 | ) 170 | losses = -( 171 | targets * torch.log(probabilities + epsilon) 172 | + (1.0 - targets) * torch.log(1.0 - probabilities + epsilon) 173 | ) 174 | else: 175 | losses = -torch.log(probabilities + epsilon) 176 | 177 | loss = torch.mean(losses) 178 | loss.backward() 179 | 180 | # Step optimizer for learning 181 | optimizer.step() 182 | optimizer.zero_grad() 183 | 184 | # Save loss for printing 185 | total_loss += float(loss) 186 | instances_since_last_log += len(batch) 187 | 188 | # Print the loss 189 | if self.should_log(batch_idx, dataloader, epoch, epochs): 190 | self.logger.log( 191 | epoch, 192 | batch_idx, 193 | total_loss, 194 | instances_since_last_log, 195 | self.accuracy_tester_fn(), 196 | ) 197 | total_loss = 0 198 | instances_since_last_log = 0 199 | 200 | batch_idx += 1 201 | 202 | end_time = time() - training_start 203 | if self.print_time: 204 | self.logger.print( 205 | "\nTraining {} epoch (totalling {} batches of size {}) took {:.2f} seconds".format( 206 | epochs, epochs * len(dataloader), dataloader.batch_size, end_time 207 | ) 208 | ) 209 | 210 | # Test the performance on the first test query again 211 | if self.test_query is not None: 212 | self.test_query() 213 | 214 | return end_time 215 | 216 | def should_log(self, batch_idx, dataloader, epoch, epochs): 217 | return batch_idx % self.log_freq == 0 or ( 218 | epoch == epochs - 1 and batch_idx % len(dataloader) == len(dataloader) - 1 219 | ) 220 | -------------------------------------------------------------------------------- /examples/addition/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ML-KULeuven/deepstochlog/aed95319411b8b5190b5b418b65b523b1436bcfd/examples/addition/__init__.py -------------------------------------------------------------------------------- /examples/addition/addition.pl: -------------------------------------------------------------------------------- 1 | digit(Y) :- member(Y,[0,1,2,3,4,5,6,7,8,9]). 2 | nn(number, [X], Y, digit) :: is_number(Y) --> [X]. 3 | addition(N) --> is_number(N1), 4 | is_number(N2), 5 | {N is N1 + N2}. 6 | multi_addition(N, 1) --> addition(N). 7 | multi_addition(N, L) --> {L > 1, L2 is L - 1}, 8 | addition(N1), 9 | multi_addition(N2, L2), 10 | {N is N1*(10**L2) + N2}. -------------------------------------------------------------------------------- /examples/addition/addition.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Iterable 3 | import numpy as np 4 | 5 | from examples.addition.addition_data import AdditionDataset, argument_lists 6 | import torch 7 | from torch.optim import Adam 8 | from time import time 9 | 10 | from deepstochlog.network import Network, NetworkStore 11 | from examples.models import MNISTNet 12 | from deepstochlog.utils import ( 13 | set_fixed_seed, 14 | create_run_test_query, 15 | create_model_accuracy_calculator, 16 | ) 17 | from deepstochlog.dataloader import DataLoader 18 | from deepstochlog.model import DeepStochLogModel 19 | from deepstochlog.term import Term, List 20 | from deepstochlog.trainer import DeepStochLogTrainer, print_logger 21 | from deepstochlog.logic import NNLeaf, LogicNode 22 | 23 | root_path = Path(__file__).parent 24 | 25 | 26 | class GreedyEvaluation: 27 | def __init__(self, digit_length, valid_data, test_data, store, device): 28 | 29 | self.digit_length = digit_length 30 | self.valid_data = valid_data 31 | self.test_data = test_data 32 | self.store = store 33 | self.header = "Val acc\tTest acc" 34 | self.max_val = 0.0 35 | self.test_acc = 0.0 36 | self.device = device 37 | 38 | def _addition(self, l, n): 39 | res = 0 40 | for i in range(len(l) // 2): 41 | n = n - 1 42 | li = [l[i], l[i + self.digit_length]] 43 | res = res + sum(li) * (10 ** (n)) 44 | return int(res) 45 | 46 | def _acc(self, data, number): 47 | evaluations = [] 48 | 49 | for term in data: 50 | res = [] 51 | for i, (id, tensor) in enumerate(term.context._context.items()): 52 | n = torch.argmax( 53 | number.neural_model(tensor.unsqueeze(dim=0).to(self.device)).cpu() 54 | ).numpy() 55 | res.append(n) 56 | 57 | res = self._addition(res, self.digit_length) 58 | ground = int(term.term.arguments[0].functor) 59 | evaluations.append(int(ground == res)) 60 | s = np.mean(evaluations) 61 | return s 62 | 63 | def __call__(self): 64 | number = self.store.networks["number"] 65 | number.neural_model.eval() 66 | 67 | valid_acc = self._acc(self.valid_data, number) 68 | if valid_acc >= self.max_val: 69 | self.test_acc = self._acc(self.test_data, number) 70 | self.max_val = valid_acc 71 | 72 | number.neural_model.train() 73 | return "%s\t%s\t" % (str(valid_acc), str(self.test_acc)) 74 | 75 | 76 | def create_parse(term: Term, logic_node: Iterable[LogicNode], networks: NetworkStore): 77 | elements = dict() 78 | for nnleaf in logic_node: 79 | # elements[nnleaf.inputs[0]] = networks.get_network(nnleaf.network).idx2term( 80 | # nnleaf.index 81 | # ) 82 | if isinstance(nnleaf, NNLeaf): 83 | elements[nnleaf.inputs[0].functor] = str(nnleaf.index) 84 | 85 | digits = [] 86 | for token in sorted(elements.keys()): 87 | digits.append(elements[token]) 88 | result = ( 89 | "".join(digits[: len(digits) // 2]) + "+" + "".join(digits[len(digits) // 2 :]) 90 | ) 91 | return result 92 | 93 | 94 | def run( 95 | digit_length=1, 96 | epochs=2, 97 | batch_size=32, 98 | lr=1e-3, 99 | greedy=False, 100 | # 101 | train_size=None, 102 | val_size=500, 103 | test_size=None, 104 | # 105 | log_freq=100, 106 | logger=print_logger, 107 | test_example_idx=None, 108 | test_batch_size=100, 109 | val_batch_size=100, 110 | most_probable_parse_accuracy=False, 111 | # 112 | seed=None, 113 | verbose=False, 114 | verbose_building=False, 115 | ): 116 | start_time = time() 117 | # Setting seed for reproducibility 118 | set_fixed_seed(seed) 119 | 120 | # Create a network object, containing the MNIST network and the index list 121 | mnist_classifier = Network( 122 | "number", MNISTNet(), index_list=[Term(str(i)) for i in range(10)] 123 | ) 124 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 125 | networks = NetworkStore(mnist_classifier) 126 | 127 | # Load the model "addition.pl" with this MNIST network 128 | query = Term( 129 | "multi_addition", 130 | Term("_"), 131 | Term(str(digit_length)), 132 | argument_lists[digit_length], 133 | ) 134 | 135 | grounding_start = time() 136 | model = DeepStochLogModel.from_file( 137 | file_location=str((root_path / "addition.pl").absolute()), 138 | query=query, 139 | networks=networks, 140 | device=device, 141 | verbose=verbose_building, 142 | ) 143 | optimizer = Adam(model.get_all_net_parameters(), lr=lr) 144 | optimizer.zero_grad() 145 | 146 | grounding_time = time() - grounding_start 147 | if verbose: 148 | print("Grounding took", grounding_time, "seconds") 149 | 150 | train_and_val_data = AdditionDataset( 151 | True, 152 | digit_length=digit_length, 153 | size=train_size + val_size if train_size else None, 154 | ) 155 | val_data = list(train_and_val_data[:val_size]) 156 | train_data = list(train_and_val_data[val_size:]) 157 | test_data = AdditionDataset(False, digit_length=digit_length, size=test_size) 158 | 159 | # Own DataLoader that can deal with proof trees and tensors (replicates the pytorch dataloader interface) 160 | train_dataloader = DataLoader(train_data, batch_size=batch_size) 161 | val_dataloader = DataLoader(val_data, batch_size=val_batch_size) 162 | test_dataloader = DataLoader(test_data, batch_size=test_batch_size) 163 | 164 | if greedy: 165 | g = GreedyEvaluation(digit_length, val_data, test_data, networks, device=device) 166 | calculate_model_accuracy = g.header, g 167 | 168 | # Train the DeepStochLog model 169 | trainer = DeepStochLogTrainer( 170 | log_freq=log_freq, 171 | accuracy_tester=calculate_model_accuracy, 172 | logger=logger, 173 | print_time=verbose, 174 | ) 175 | 176 | else: 177 | # Create test functions 178 | run_test_query = create_run_test_query( 179 | model=model, 180 | test_data=test_data, 181 | test_example_idx=test_example_idx, 182 | verbose=verbose, 183 | create_parse=create_parse, 184 | ) 185 | calculate_model_accuracy = create_model_accuracy_calculator( 186 | model, 187 | test_dataloader, 188 | start_time, 189 | val_dataloader=val_dataloader, 190 | create_parse=create_parse, 191 | most_probable_parse_accuracy=most_probable_parse_accuracy, 192 | ) 193 | 194 | # Train the DeepStochLog model 195 | trainer = DeepStochLogTrainer( 196 | log_freq=log_freq, 197 | accuracy_tester=calculate_model_accuracy, 198 | logger=logger, 199 | print_time=verbose, 200 | test_query=run_test_query, 201 | ) 202 | 203 | end_time = trainer.train( 204 | model=model, 205 | optimizer=optimizer, 206 | dataloader=train_dataloader, 207 | epochs=epochs, 208 | ) 209 | 210 | return { 211 | "proving_time": grounding_time, 212 | "neural_time": end_time, 213 | } 214 | 215 | 216 | if __name__ == "__main__": 217 | run( 218 | test_example_idx=[0, 1, 2, 3, 4], 219 | seed=42, 220 | verbose=True, 221 | val_size=500, 222 | train_size=5000, 223 | test_size=100, 224 | digit_length=1, 225 | epochs=2, 226 | greedy=True, 227 | ) 228 | -------------------------------------------------------------------------------- /examples/addition/addition_data.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | from typing import Union 3 | 4 | import typing 5 | from torch import Tensor 6 | 7 | from examples.data_utils import get_mnist_data 8 | from deepstochlog.context import ContextualizedTerm, Context 9 | from deepstochlog.term import Term, List 10 | 11 | 12 | def calculate_number_from_digits(digit_tensors: typing.Tuple[int, ...]): 13 | digits = [d for d in digit_tensors] 14 | numbers_base_10 = sum( 15 | digit * pow(10, len(digit_tensors) - i - 1) for (i, digit) in enumerate(digits) 16 | ) 17 | return numbers_base_10 18 | 19 | 20 | class AdditionDataset(Sequence): 21 | def __init__(self, train: bool, digit_length=1, size: int = None): 22 | self.mnist_dataset = get_mnist_data(train) 23 | self.ct_term_dataset = [] 24 | size = self.calculate_dataset_size(size=size, digit_length=digit_length) 25 | for idx in range(0, 2 * digit_length * size, 2 * digit_length): 26 | data_points = self.get_mnist_datapoints(digit_length, idx) 27 | tensors, digits = zip(*data_points) 28 | number_1 = calculate_number_from_digits(digits[:digit_length]) 29 | number_2 = calculate_number_from_digits(digits[digit_length:]) 30 | total_sum = number_1 + number_2 31 | addition_term = create_addition_term( 32 | total_sum=total_sum, digit_length=digit_length, tensors=tensors, number_1=number_1, number_2=number_2 33 | ) 34 | self.ct_term_dataset.append(addition_term) 35 | 36 | def get_mnist_datapoints(self, digit_length, idx): 37 | return [self.mnist_dataset[i] for i in range(idx, idx + 2 * digit_length)] 38 | 39 | def calculate_dataset_size(self, size: int, digit_length: int): 40 | return ( 41 | len(self.mnist_dataset) // (digit_length * 2) 42 | if size is None or size > (len(self.mnist_dataset) // 2) 43 | else size 44 | ) 45 | 46 | def __len__(self): 47 | return len(self.ct_term_dataset) 48 | 49 | def __getitem__(self, item: Union[int, slice]): 50 | if type(item) is slice: 51 | return (self[i] for i in range(*item.indices(len(self)))) 52 | return self.ct_term_dataset[item] 53 | 54 | 55 | max_digit_length = 4 56 | terms = [Term("t" + str(i)) for i in range(1, 2 * max_digit_length + 1)] 57 | 58 | 59 | def create_argument_list(digit_length: int): 60 | args = [] 61 | for i in range(digit_length): 62 | args.append(terms[i]) 63 | args.append(terms[digit_length + i]) 64 | return List(*args) 65 | 66 | 67 | argument_lists = [ 68 | create_argument_list(digit_length=digit_length) 69 | for digit_length in range(max_digit_length + 1) 70 | ] 71 | length_arguments = [Term(str(i)) for i in range(max_digit_length + 1)] 72 | 73 | 74 | def create_addition_term( 75 | total_sum: int, 76 | digit_length, 77 | tensors: typing.Iterable[Tensor], 78 | number_1: int, 79 | number_2: int, 80 | ): 81 | context = {} 82 | argument_list = argument_lists[digit_length] 83 | for i, tensor in enumerate(tensors): 84 | context[terms[i]] = tensor 85 | 86 | return ContextualizedTerm( 87 | context=Context(context), 88 | term=Term( 89 | "multi_addition", 90 | Term(str(total_sum)), 91 | length_arguments[digit_length], 92 | argument_list, 93 | ), 94 | meta=str(number_1) + "+" + str(number_2), 95 | ) 96 | -------------------------------------------------------------------------------- /examples/addition/addition_evaluate.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from examples.addition import addition 4 | from examples.evaluate import evaluate_deepstochlog_task, create_default_parser 5 | 6 | eval_root = Path(__file__).parent 7 | 8 | 9 | def main( 10 | digit_length: int = 1, 11 | epochs: int = 5, 12 | runs: int = 5, 13 | only_time: bool = False, 14 | greedy: bool = False, 15 | ): 16 | 17 | print( 18 | "Running {} runs of {} epochs each for digit length {}".format( 19 | runs, epochs, digit_length 20 | ) 21 | ) 22 | if greedy: 23 | print("Greedy") 24 | 25 | arguments = { 26 | "digit_length": digit_length, 27 | "epochs": epochs, 28 | "val_size": 500, 29 | "test_size": None, 30 | "log_freq": 100, 31 | "test_example_idx": [], 32 | # "seed": 42, 33 | "verbose": False, 34 | "greedy": greedy, 35 | } 36 | 37 | evaluate_deepstochlog_task( 38 | runner=addition.run, 39 | arguments=arguments, 40 | runs=runs, 41 | only_time=only_time, 42 | name=("addition" + str(digit_length)), 43 | maximize_attr=("Val acc", "Val P(cor)") if not greedy else ("Val acc",), 44 | target_attr=("Test acc", "Test P(cor)", "time") 45 | if not greedy 46 | else ("Test acc",), 47 | ) 48 | 49 | 50 | parser = create_default_parser() 51 | parser.add_argument("-d", "--digit_length", type=int, default=1) 52 | parser.add_argument("-g", "--greedy", default=False, action="store_true") 53 | 54 | if __name__ == "__main__": 55 | args = parser.parse_args() 56 | main( 57 | digit_length=args.digit_length, 58 | epochs=args.epochs, 59 | runs=args.runs, 60 | only_time=args.only_time, 61 | greedy=args.greedy, 62 | ) 63 | -------------------------------------------------------------------------------- /examples/addition/addition_timing.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Iterable 3 | import numpy as np 4 | 5 | from examples.addition.addition_data import AdditionDataset, argument_lists 6 | import torch 7 | from torch.optim import Adam 8 | from time import time 9 | 10 | from deepstochlog.network import Network, NetworkStore 11 | from examples.models import MNISTNet 12 | from deepstochlog.utils import ( 13 | set_fixed_seed, 14 | create_run_test_query, 15 | create_model_accuracy_calculator, 16 | ) 17 | from deepstochlog.dataloader import DataLoader 18 | from deepstochlog.model import DeepStochLogModel 19 | from deepstochlog.term import Term, List 20 | from deepstochlog.trainer import DeepStochLogTrainer, print_logger 21 | from deepstochlog.logic import NNLeaf, LogicNode 22 | 23 | root_path = Path(__file__).parent 24 | 25 | 26 | class GreedyEvaluation: 27 | def __init__(self, digit_length, valid_data, test_data, store): 28 | 29 | self.digit_length = digit_length 30 | self.valid_data = valid_data 31 | self.test_data = test_data 32 | self.store = store 33 | self.header = "Valid acc\tTest acc\t" 34 | self.max_val = 0.0 35 | self.test_acc = 0.0 36 | 37 | 38 | def _addition(self, l, n): 39 | res = 0 40 | for i in range(len(l)//2): 41 | n = n-1 42 | li = [l[i], l[i+self.digit_length]] 43 | res = res + sum(li)*(10**(n)) 44 | return int(res) 45 | 46 | 47 | def _acc(self, data, number): 48 | evaluations = [] 49 | 50 | for term in data: 51 | res = [] 52 | for i, (id, tensor) in enumerate(term.context._context.items()): 53 | n = torch.argmax(number.neural_model(tensor.unsqueeze(dim=0)) ).numpy() 54 | res.append(n) 55 | 56 | res = self._addition(res, self.digit_length) 57 | ground = int(term.term.arguments[0].functor) 58 | evaluations.append(int(ground == res)) 59 | s = np.mean(evaluations) 60 | return s 61 | 62 | def __call__(self): 63 | number = self.store.networks["number"] 64 | number.neural_model.eval() 65 | 66 | valid_acc = self._acc(self.valid_data, number) 67 | if valid_acc >= self.max_val: 68 | self.test_acc = self._acc(self.test_data, number) 69 | self.max_val = valid_acc 70 | 71 | number.neural_model.train() 72 | return "%s\t%s\t" % (str(valid_acc), str(self.test_acc)) 73 | 74 | 75 | 76 | def create_parse(term: Term, logic_node: Iterable[LogicNode], networks: NetworkStore): 77 | elements = dict() 78 | for nnleaf in logic_node: 79 | # elements[nnleaf.inputs[0]] = networks.get_network(nnleaf.network).idx2term( 80 | # nnleaf.index 81 | # ) 82 | if isinstance(nnleaf, NNLeaf): 83 | elements[nnleaf.inputs[0].functor] = str(nnleaf.index) 84 | 85 | digits = [] 86 | for token in sorted(elements.keys()): 87 | digits.append(elements[token]) 88 | result = ( 89 | "".join(digits[: len(digits) // 2]) + "+" + "".join(digits[len(digits) // 2 :]) 90 | ) 91 | return result 92 | 93 | 94 | def run( 95 | digit_length=1, 96 | epochs=2, 97 | batch_size=1, 98 | lr=1e-3, 99 | # 100 | train_size=None, 101 | val_size=500, 102 | test_size=None, 103 | # 104 | log_freq=100, 105 | logger=print_logger, 106 | test_example_idx=None, 107 | test_batch_size=100, 108 | val_batch_size=100, 109 | most_probable_parse_accuracy=False, 110 | # 111 | seed=None, 112 | verbose=False, 113 | ): 114 | start_time = time() 115 | # Setting seed for reproducibility 116 | set_fixed_seed(seed) 117 | 118 | # Load the MNIST model, and Adam optimiser 119 | mnist_network = MNISTNet() 120 | 121 | # Create a network object, containing the MNIST network and the index list 122 | mnist_classifier = Network( 123 | "number", mnist_network, index_list=[Term(str(i)) for i in range(10)] 124 | ) 125 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 126 | 127 | # Load the model "addition.pl" with this MNIST network 128 | query = Term( 129 | "addition", Term("_"), Term(str(digit_length)), argument_lists[digit_length] 130 | ) 131 | networks = NetworkStore(mnist_classifier) 132 | 133 | grounding_start = time() 134 | model = DeepStochLogModel.from_file( 135 | file_location=str((root_path / "addition.pl").absolute()), 136 | query=query, 137 | networks=networks, 138 | device=device, 139 | ) 140 | optimizer = Adam(model.get_all_net_parameters(), lr=lr) 141 | optimizer.zero_grad() 142 | 143 | grounding_time = time() - grounding_start 144 | if verbose: 145 | print("Grounding took", grounding_time, "seconds") 146 | 147 | train_and_val_data = AdditionDataset( 148 | True, 149 | digit_length=digit_length, 150 | size=train_size + val_size if train_size else None, 151 | ) 152 | train_data = list(train_and_val_data[val_size:]) 153 | 154 | # Own DataLoader that can deal with proof trees and tensors (replicates the pytorch dataloader interface) 155 | train_dataloader = DataLoader(train_data, batch_size=batch_size) 156 | 157 | 158 | run_times = [] 159 | i = 0 160 | for batch in train_dataloader: 161 | i+=1 162 | if i==100: break 163 | # Cross-Entropy (CE) loss 164 | before = time() 165 | probabilities = model.predict_sum_product(batch) 166 | run_times.append(time() - before) 167 | 168 | print(digit_length, np.mean(run_times), np.std(run_times)) 169 | 170 | return digit_length, np.mean(run_times), np.std(run_times) 171 | 172 | if __name__ == "__main__": 173 | 174 | 175 | 176 | for l in [1,2,3,4]: 177 | run( 178 | test_example_idx=[0], 179 | seed=0, 180 | verbose=True, 181 | # val_size=500, 182 | # train_size=5000, 183 | # test_size=100, 184 | digit_length=l, 185 | epochs=50, 186 | ) 187 | -------------------------------------------------------------------------------- /examples/addition_simple/addition_simple.pl: -------------------------------------------------------------------------------- 1 | digit_dom(Y) :- member(Y,[0,1,2,3,4,5,6,7,8,9]). 2 | 3 | nn(number, [X], Y, digit_dom) :: digit(Y) --> [X]. 4 | 1 :: addition(N) --> digit(N1), 5 | digit(N2), 6 | {N is N1 + N2}. -------------------------------------------------------------------------------- /examples/addition_simple/addition_simple.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Iterable, Sequence, Union, Tuple 3 | 4 | from torch.tensor import Tensor 5 | 6 | from deepstochlog.context import Context, ContextualizedTerm 7 | import torch 8 | from torch.optim import Adam 9 | from time import time 10 | 11 | from deepstochlog.network import Network, NetworkStore 12 | from examples.data_utils import get_mnist_data 13 | from examples.models import MNISTNet 14 | from deepstochlog.utils import ( 15 | set_fixed_seed, 16 | create_run_test_query, 17 | create_model_accuracy_calculator, 18 | ) 19 | from deepstochlog.dataloader import DataLoader 20 | from deepstochlog.model import DeepStochLogModel 21 | from deepstochlog.term import Term, List 22 | from deepstochlog.trainer import DeepStochLogTrainer, print_logger 23 | 24 | root_path = Path(__file__).parent 25 | 26 | 27 | t1 = Term("t1") 28 | t2 = Term("t2") 29 | argument_sequence = List(t1, t2) 30 | 31 | 32 | class SimpleAdditionDataset(Sequence): 33 | def __init__(self, train: bool, digit_length=1, size: int = None): 34 | self.mnist_dataset = get_mnist_data(train) 35 | self.ct_term_dataset = [] 36 | size = len(self.mnist_dataset) // 2 37 | for idx in range(0, 2 * size, 2): 38 | mnist_datapoint_1: Tuple[Tensor, int] = self.mnist_dataset[idx] 39 | mnist_datapoint_2: Tuple[Tensor, int] = self.mnist_dataset[idx + 1] 40 | digit_1 = mnist_datapoint_1[1] 41 | digit_2 = mnist_datapoint_2[1] 42 | total_sum = mnist_datapoint_1[1] + mnist_datapoint_2[1] 43 | 44 | addition_term = ContextualizedTerm( 45 | # Load context with the tensors 46 | context=Context({t1: mnist_datapoint_1[0], t2: mnist_datapoint_2[0]}), 47 | # Create the term containing the sum and a list of tokens representing the tensors 48 | term=Term( 49 | "addition", 50 | Term(str(total_sum)), 51 | argument_sequence, 52 | ), 53 | meta=str(digit_1) + "+" + str(digit_2), 54 | ) 55 | self.ct_term_dataset.append(addition_term) 56 | 57 | def __len__(self): 58 | return len(self.ct_term_dataset) 59 | 60 | def __getitem__(self, item: Union[int, slice]): 61 | if type(item) is slice: 62 | return (self[i] for i in range(*item.indices(len(self)))) 63 | return self.ct_term_dataset[item] 64 | 65 | 66 | def run( 67 | epochs=2, 68 | batch_size=32, 69 | lr=1e-3, 70 | # 71 | train_size=None, 72 | val_size=500, 73 | test_size=None, 74 | # 75 | log_freq=100, 76 | logger=print_logger, 77 | test_example_idx=None, 78 | val_batch_size=100, 79 | test_batch_size=100, 80 | # 81 | verbose=True, 82 | seed=None, 83 | ): 84 | start_time = time() 85 | # Setting seed for reproducibility 86 | set_fixed_seed(seed) 87 | 88 | # Create a network object, containing the MNIST network and the index list 89 | mnist_classifier = Network( 90 | "number", MNISTNet(), index_list=[Term(str(i)) for i in range(10)] 91 | ) 92 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 93 | networks = NetworkStore(mnist_classifier) 94 | 95 | # Load the model "addition_simple.pl" with the specific query 96 | query = Term( 97 | # We want to use the "addition" non-terminal in the specific grammar 98 | "addition", 99 | # We want to calculate all possible sums by giving the wildcard "_" as argument 100 | Term("_"), 101 | # Denote that the input will be a list of two tensors, t1 and t2, representing the MNIST digit. 102 | List(Term("t1"), Term("t2")), 103 | ) 104 | model = DeepStochLogModel.from_file( 105 | file_location=str((root_path / "addition_simple.pl").absolute()), 106 | query=query, 107 | networks=networks, 108 | device=device, 109 | verbose=verbose, 110 | ) 111 | optimizer = Adam(model.get_all_net_parameters(), lr=lr) 112 | optimizer.zero_grad() 113 | 114 | train_and_val_data = SimpleAdditionDataset( 115 | True, 116 | digit_length=1, 117 | size=train_size + val_size if train_size else None, 118 | ) 119 | val_data = list(train_and_val_data[:val_size]) 120 | train_data = list(train_and_val_data[val_size:]) 121 | test_data = SimpleAdditionDataset(False, digit_length=1, size=test_size) 122 | 123 | # Own DataLoader that can deal with proof trees and tensors (replicates the pytorch dataloader interface) 124 | train_dataloader = DataLoader(train_data, batch_size=batch_size) 125 | val_dataloader = DataLoader(val_data, batch_size=val_batch_size) 126 | test_dataloader = DataLoader(test_data, batch_size=test_batch_size) 127 | 128 | # Create test functions 129 | run_test_query = create_run_test_query( 130 | model=model, 131 | test_data=test_data, 132 | test_example_idx=test_example_idx, 133 | verbose=verbose, 134 | ) 135 | calculate_model_accuracy = create_model_accuracy_calculator( 136 | model, 137 | test_dataloader, 138 | start_time, 139 | val_dataloader=val_dataloader, 140 | ) 141 | 142 | # Train the DeepStochLog model 143 | trainer = DeepStochLogTrainer( 144 | log_freq=log_freq, 145 | accuracy_tester=calculate_model_accuracy, 146 | logger=logger, 147 | print_time=verbose, 148 | test_query=run_test_query, 149 | ) 150 | 151 | trainer.train( 152 | model=model, 153 | optimizer=optimizer, 154 | dataloader=train_dataloader, 155 | epochs=epochs, 156 | ) 157 | 158 | print("Done running DeepStochLog simple addition example") 159 | 160 | 161 | if __name__ == "__main__": 162 | run( 163 | test_example_idx=[0, 1, 2], 164 | seed=42, 165 | verbose=True, 166 | val_size=500, 167 | train_size=5000, 168 | test_size=100, 169 | epochs=2, 170 | ) 171 | -------------------------------------------------------------------------------- /examples/anbncn/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ML-KULeuven/deepstochlog/aed95319411b8b5190b5b418b65b523b1436bcfd/examples/anbncn/__init__.py -------------------------------------------------------------------------------- /examples/anbncn/anbncn.pl: -------------------------------------------------------------------------------- 1 | letter(X) :- member(X, [a,b,c]). 2 | 3 | 0.5 :: s(0) --> akblcm(K,L,M), 4 | {K\=L; L\=M; M\=K}, 5 | {K \= 0, L \= 0, M \= 0}. 6 | 0.5 :: s(1) --> akblcm(N,N,N). 7 | 8 | akblcm(K,L,M) --> rep(K,A), 9 | rep(L,B), 10 | rep(M,C), 11 | {A\=B, B\=C, C\=A}. 12 | 13 | rep(0, _) --> []. 14 | nn(mnist, [X], C, letter) :: rep(s(N), C) --> [X], 15 | rep(N,C). 16 | -------------------------------------------------------------------------------- /examples/anbncn/anbncn.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Optional 3 | 4 | import torch 5 | from torch.optim import Adam 6 | from time import time 7 | 8 | from examples.anbncn.anbncn_data import ABCDataset 9 | from deepstochlog.network import Network, NetworkStore 10 | from examples.models import MNISTNet 11 | from deepstochlog.utils import ( 12 | set_fixed_seed, 13 | create_run_test_query, 14 | create_model_accuracy_calculator, 15 | ) 16 | from deepstochlog.dataloader import DataLoader 17 | from deepstochlog.model import DeepStochLogModel 18 | from deepstochlog.term import Term, List 19 | from deepstochlog.trainer import DeepStochLogTrainer, print_logger 20 | 21 | root_path = Path(__file__).parent 22 | 23 | 24 | def run( 25 | min_length: int = 3, 26 | max_length: int = 21, 27 | allow_non_threefold: float = False, 28 | # 29 | epochs=1, 30 | batch_size=32, 31 | lr=1e-3, 32 | # 33 | train_size=None, 34 | val_size=None, 35 | test_size=None, 36 | # 37 | log_freq=50, 38 | logger=print_logger, 39 | test_example_idx=None, 40 | test_batch_size=100, 41 | val_batch_size=100, 42 | val_num_digits=300, 43 | most_probable_parse_accuracy=False, 44 | # 45 | seed=None, 46 | set_program_seed=True, 47 | verbose=True, 48 | verbose_building=False, 49 | ): 50 | if max_length < min_length: 51 | raise RuntimeError( 52 | "Max length can not be larger than minimum length:" 53 | + str(max_length) 54 | + "<" 55 | + str(min_length) 56 | ) 57 | 58 | start_time = time() 59 | # Setting seed for reproducibility 60 | if set_program_seed: 61 | set_fixed_seed(seed) 62 | 63 | # Load the MNIST model, and Adam optimiser 64 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 65 | mnist_network = MNISTNet(output_features=3) 66 | 67 | # Create a network object, containing the MNIST network and the index list 68 | mnist_classifier = Network( 69 | "mnist", mnist_network, index_list=[Term("a"), Term("b"), Term("c")] 70 | ) 71 | networks = NetworkStore(mnist_classifier) 72 | 73 | # Load the model "addition.pl" with this MNIST network 74 | queries = [ 75 | Term("s", Term("_"), List(*[Term("t" + str(i)) for i in range(length)])) 76 | for length in range(min_length, max_length + 1) 77 | if allow_non_threefold or length % 3 == 0 78 | ] 79 | grounding_start_time = time() 80 | model = DeepStochLogModel.from_file( 81 | file_location=str((root_path / "anbncn.pl").absolute()), 82 | query=queries, 83 | networks=networks, 84 | device=device, 85 | verbose=verbose_building, 86 | ) 87 | optimizer = Adam(model.get_all_net_parameters(), lr=lr) 88 | optimizer.zero_grad() 89 | grounding_time = time() - grounding_start_time 90 | if verbose: 91 | print("Grounding the program took {:.3} seconds".format(grounding_time)) 92 | 93 | train_data = ABCDataset( 94 | split="train", 95 | size=train_size, 96 | min_length=min_length, 97 | max_length=max_length, 98 | allow_non_threefold=allow_non_threefold, 99 | seed=seed, 100 | val_num_digits=val_num_digits, 101 | ) 102 | val_data = ABCDataset( 103 | split="val", 104 | size=val_size, 105 | min_length=min_length, 106 | max_length=max_length, 107 | allow_non_threefold=allow_non_threefold, 108 | seed=seed, 109 | val_num_digits=val_num_digits, 110 | ) 111 | # if val_size is None: 112 | # val_size = train_size // 10 113 | # val_data = ABCDataset( 114 | # split="train", 115 | # size=val_size, 116 | # min_length=min_length, 117 | # max_length=max_length, 118 | # allow_non_threefold=allow_non_threefold, 119 | # seed=seed, 120 | # ) 121 | test_data = ABCDataset( 122 | split="test", 123 | size=test_size, 124 | min_length=min_length, 125 | max_length=max_length, 126 | allow_non_threefold=allow_non_threefold, 127 | seed=seed, 128 | ) 129 | 130 | # Own DataLoader that can deal with proof trees and tensors (replicates the pytorch dataloader interface) 131 | train_dataloader = DataLoader( 132 | train_data, 133 | batch_size=batch_size, 134 | ) 135 | val_dataloader = DataLoader(val_data, batch_size=val_batch_size) 136 | test_dataloader = DataLoader(test_data, batch_size=test_batch_size) 137 | # val_dataloader = DataLoader( 138 | # val_data, val_batch_size if val_batch_size is not None else val_size 139 | # ) 140 | 141 | # Create test functions 142 | # run_test_query = create_run_test_query_probability( 143 | # model, test_data, test_example_idx, verbose 144 | # ) 145 | # calculate_model_accuracy = create_probability_accuracy_calculator( 146 | # model, 147 | # test_dataloader, 148 | # start_time, 149 | # threshold=threshold, 150 | # validation_data=val_dataloader, 151 | # most_probable_parse_accuracy=False, 152 | # ) 153 | 154 | run_test_query = create_run_test_query( 155 | model=model, 156 | test_data=test_data, 157 | test_example_idx=test_example_idx, 158 | verbose=verbose, 159 | ) 160 | calculate_model_accuracy = create_model_accuracy_calculator( 161 | model, 162 | test_dataloader, 163 | start_time, 164 | most_probable_parse_accuracy=most_probable_parse_accuracy, 165 | val_dataloader=val_dataloader, 166 | ) 167 | 168 | # Train the DeepStochLog model 169 | trainer = DeepStochLogTrainer( 170 | log_freq=log_freq, 171 | accuracy_tester=calculate_model_accuracy, 172 | logger=logger, 173 | test_query=run_test_query, 174 | print_time=verbose, 175 | ) 176 | train_time = trainer.train( 177 | model=model, optimizer=optimizer, dataloader=train_dataloader, epochs=epochs 178 | ) 179 | 180 | return { 181 | "proving_time": grounding_time, 182 | "neural_time": train_time, 183 | "model": model, 184 | } 185 | 186 | 187 | if __name__ == "__main__": 188 | run( 189 | min_length=3, 190 | max_length=12, 191 | train_size=4000, 192 | val_size=100, 193 | test_size=200, 194 | val_num_digits=300, 195 | allow_non_threefold=False, 196 | log_freq=10, 197 | epochs=1, 198 | test_example_idx=[0, 1, 2, 3], 199 | seed=42, 200 | verbose_building=False, 201 | ) 202 | -------------------------------------------------------------------------------- /examples/anbncn/anbncn_evaluate.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from examples.anbncn import anbncn 4 | from examples.evaluate import evaluate_deepstochlog_task, create_default_parser 5 | 6 | eval_root = Path(__file__).parent 7 | 8 | 9 | def main(max_length: int = 12, epochs: int = 1, runs: int = 5, only_time: bool = False): 10 | 11 | print( 12 | "Running {} runs of {} epochs each for max length {}".format( 13 | runs, epochs, max_length 14 | ) 15 | ) 16 | arguments = { 17 | "min_length": 3, 18 | "max_length": max_length, 19 | "train_size": 4000, 20 | "batch_size": 4, 21 | "val_size": 100, 22 | "test_size": 200, 23 | "val_num_digits": 300, 24 | "allow_non_threefold": False, 25 | "seed": 42, 26 | "set_program_seed": False, 27 | # 28 | "epochs": epochs, 29 | "log_freq": 50, 30 | "test_example_idx": [], 31 | "verbose": False, 32 | } 33 | 34 | evaluate_deepstochlog_task( 35 | runner=anbncn.run, 36 | arguments=arguments, 37 | runs=runs, 38 | name=("anbcn" + str(max_length)), 39 | only_time=only_time, 40 | maximize_attr=( 41 | "Val acc", 42 | "Val P(cor)", 43 | # "Val parse acc", 44 | ), 45 | target_attr=("Test acc", "Test P(cor)", 46 | # "Test parse acc", 47 | "time"), 48 | ) 49 | 50 | 51 | parser = create_default_parser() 52 | parser.add_argument("-l", "--max_length", type=int, default=12) 53 | 54 | if __name__ == "__main__": 55 | args = parser.parse_args() 56 | main( 57 | max_length=args.max_length, 58 | epochs=args.epochs, 59 | runs=args.runs, 60 | only_time=args.only_time, 61 | ) 62 | -------------------------------------------------------------------------------- /examples/bracket/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ML-KULeuven/deepstochlog/aed95319411b8b5190b5b418b65b523b1436bcfd/examples/bracket/__init__.py -------------------------------------------------------------------------------- /examples/bracket/bracket.pl: -------------------------------------------------------------------------------- 1 | brackets_dom(X) :- member(X, ["(",")"]). 2 | nn(bracket_nn, [X], Y, brackets_dom) :: bracket(Y) --> [X]. 3 | 4 | t(_) :: s --> s, s. 5 | t(_) :: s --> bracket("("), s, bracket(")"). 6 | t(_) :: s --> bracket("("), bracket(")"). -------------------------------------------------------------------------------- /examples/bracket/bracket.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Optional 3 | 4 | import torch 5 | from torch.optim import Adam 6 | from time import time 7 | 8 | from examples.bracket.bracket_data import BracketDataset 9 | from deepstochlog.network import Network, NetworkStore 10 | from examples.models import MNISTNet 11 | from deepstochlog.nn_models import TrainableProbability 12 | from deepstochlog.utils import ( 13 | set_fixed_seed, 14 | create_run_test_query, 15 | create_model_accuracy_calculator, 16 | ) 17 | from deepstochlog.dataloader import DataLoader 18 | from deepstochlog.model import DeepStochLogModel 19 | from deepstochlog.term import Term, List 20 | from deepstochlog.trainer import DeepStochLogTrainer, print_logger 21 | 22 | root_path = Path(__file__).parent 23 | 24 | 25 | def run( 26 | min_length: int = 2, 27 | max_length: int = 8, 28 | allow_uneven: float = False, 29 | only_bracket_language_examples=True, 30 | # 31 | epochs=2, 32 | batch_size=32, 33 | lr=1e-3, 34 | # 35 | train_size=None, 36 | val_size=None, 37 | test_size=None, 38 | # allow_negative_during_training=True, 39 | # allow_negative_during_testing=True, 40 | # 41 | log_freq=50, 42 | logger=print_logger, 43 | test_example_idx=None, 44 | test_batch_size=100, 45 | val_batch_size=100, 46 | val_num_digits=1000, 47 | # 48 | seed=None, 49 | set_program_seed=True, 50 | verbose=True, 51 | verbose_building=False, 52 | ): 53 | start_time = time() 54 | # Setting seed for reproducibility 55 | if set_program_seed: 56 | set_fixed_seed(seed) 57 | 58 | # Load the MNIST model, and Adam optimiser 59 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 60 | mnist_network = MNISTNet(output_features=2) 61 | 62 | # Create a network object, containing the MNIST network and the index list 63 | mnist_classifier = Network( 64 | "bracket_nn", mnist_network, index_list=[Term("("), Term(")")] 65 | ) 66 | networks = NetworkStore(mnist_classifier) 67 | 68 | # Load the model "addition.pl" with this MNIST network 69 | possible_lengths = range(min_length, max_length + 1, 1 if allow_uneven else 2) 70 | if only_bracket_language_examples: 71 | queries = [ 72 | Term("s", List(*[Term("t" + str(i)) for i in range(length)])) 73 | for length in possible_lengths 74 | ] 75 | else: 76 | queries = [ 77 | Term("s", Term("_"), List(*[Term("t" + str(i)) for i in range(length)])) 78 | for length in possible_lengths 79 | ] 80 | grounding_start_time = time() 81 | model = DeepStochLogModel.from_file( 82 | file_location=str( 83 | ( 84 | root_path 85 | / ( 86 | "bracket.pl" 87 | if only_bracket_language_examples 88 | else "bracket_all.pl" 89 | ) 90 | ).absolute() 91 | ), 92 | query=queries, 93 | networks=networks, 94 | device=device, 95 | verbose=verbose_building, 96 | ) 97 | optimizer = Adam(model.get_all_net_parameters(), lr=lr) 98 | optimizer.zero_grad() 99 | 100 | grounding_time = time() - grounding_start_time 101 | if verbose: 102 | print("Grounding the program took {:.3} seconds".format(grounding_time)) 103 | 104 | train_data = BracketDataset( 105 | split="train", 106 | size=train_size, 107 | min_length=min_length, 108 | max_length=max_length, 109 | allow_uneven=allow_uneven, 110 | seed=seed, 111 | val_num_digits=val_num_digits, 112 | only_bracket_language_examples=only_bracket_language_examples, 113 | ) 114 | val_data = BracketDataset( 115 | split="val", 116 | size=val_size, 117 | min_length=min_length, 118 | max_length=max_length, 119 | allow_uneven=allow_uneven, 120 | seed=seed, 121 | val_num_digits=val_num_digits, 122 | only_bracket_language_examples=only_bracket_language_examples, 123 | ) 124 | test_data = BracketDataset( 125 | split="test", 126 | size=test_size, 127 | min_length=min_length, 128 | max_length=max_length, 129 | allow_uneven=allow_uneven, 130 | seed=seed, 131 | only_bracket_language_examples=only_bracket_language_examples, 132 | ) 133 | 134 | # Own DataLoader that can deal with proof trees and tensors (replicates the pytorch dataloader interface) 135 | train_dataloader = DataLoader( 136 | train_data, 137 | batch_size=batch_size, 138 | ) 139 | test_dataloader = DataLoader(test_data, batch_size=test_batch_size) 140 | val_dataloader = DataLoader(val_data, val_batch_size) 141 | 142 | # Create test functions 143 | # run_test_query = create_run_test_query_probability( 144 | # model, test_data, test_example_idx, verbose 145 | # ) 146 | # calculate_model_accuracy = create_probability_accuracy_calculator( 147 | # model, 148 | # test_dataloader, 149 | # start_time, 150 | # threshold=threshold, 151 | # validation_data=val_dataloader, 152 | # most_probable_parse_accuracy=True, 153 | # ) 154 | run_test_query = create_run_test_query( 155 | model=model, 156 | test_data=test_data, 157 | test_example_idx=test_example_idx, 158 | verbose=verbose, 159 | generation_output_accuracy=False, 160 | ) 161 | calculate_model_accuracy = create_model_accuracy_calculator( 162 | model, 163 | test_dataloader, 164 | start_time, 165 | generation_output_accuracy=False, 166 | most_probable_parse_accuracy=True, 167 | val_dataloader=val_dataloader if val_num_digits > 0 else None, 168 | ) 169 | 170 | # Train the DeepStochLog model 171 | trainer = DeepStochLogTrainer( 172 | log_freq=log_freq, 173 | accuracy_tester=calculate_model_accuracy, 174 | logger=logger, 175 | test_query=run_test_query, 176 | print_time=verbose, 177 | ) 178 | train_time = trainer.train( 179 | model=model, optimizer=optimizer, dataloader=train_dataloader, epochs=epochs 180 | ) 181 | 182 | return { 183 | "grounding_time": grounding_time, 184 | "training_time": train_time, 185 | "model": model, 186 | } 187 | 188 | 189 | if __name__ == "__main__": 190 | run( 191 | min_length=2, 192 | max_length=10, 193 | train_size=1000, 194 | val_size=200, 195 | test_size=200, 196 | allow_uneven=False, 197 | epochs=1, 198 | test_example_idx=[0, 1, 2, 3], 199 | seed=42, 200 | log_freq=10, 201 | only_bracket_language_examples=True, 202 | ) 203 | -------------------------------------------------------------------------------- /examples/bracket/bracket_all.pl: -------------------------------------------------------------------------------- 1 | bracket(Y) --> [X], { domain(Y,["(",")"]), nn(bracket_nn,X, Y)}. 2 | 3 | s(0) --> state(_, not_ok), {p(0.5)}. 4 | s(1) --> state(0, ok), {p(0.5)}. 5 | 6 | state(s(0), ok) --> bracket(")"), {p(0.125)}. 7 | state(X, not_ok) --> bracket(")"), {X\=s(0), p(0.05)}. 8 | state(_, not_ok) --> bracket("("), {p(0.075)}. 9 | state(X, S) --> bracket("("), state(s(X), S), {p(0.25)}. 10 | state(s(X), S) --> bracket(")"), state(X, S), {p(0.25)}. 11 | state(0, not_ok) --> bracket(")"), state(0, _), {p(0.25)}. -------------------------------------------------------------------------------- /examples/bracket/bracket_evaluate.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | from examples.bracket import bracket 5 | from examples.evaluate import evaluate_deepstochlog_task 6 | 7 | 8 | eval_root = Path(__file__).parent 9 | 10 | 11 | def main(max_length: int = 10): 12 | print("Running for max_length", max_length) 13 | arguments = { 14 | "min_length": 2, 15 | "max_length": max_length, 16 | "train_size": 1000, 17 | "val_size": 200, 18 | "test_size": 200, 19 | "batch_size": 4, 20 | "val_num_digits": 1000, 21 | "allow_uneven": False, 22 | "only_bracket_language_examples": True, 23 | "seed": 42, 24 | "set_program_seed": False, 25 | "epochs": 1, 26 | "log_freq": 5, 27 | "test_example_idx": [], 28 | "verbose": False, 29 | } 30 | 31 | evaluate_deepstochlog_task( 32 | runner=bracket.run, 33 | arguments=arguments, 34 | runs=5, 35 | name=("bracket" + str(max_length)), 36 | maximize_attr=("Val parse acc",), 37 | target_attr=("Test parse acc", "time"), 38 | ) 39 | 40 | 41 | if __name__ == "__main__": 42 | if len(sys.argv) < 2: 43 | print("Please provide the max bracket length") 44 | 45 | main(max_length=int(sys.argv[1])) 46 | -------------------------------------------------------------------------------- /examples/citeseer/base/citeseer.pl: -------------------------------------------------------------------------------- 1 | doc_neural(X,Y) --> [], {nn(classifier, [X], Y), domain(Y, [0,1,2,3,4,5])}. 2 | citep(X,Y) --> [], {cite(X,Y), findall(T, cite(X,T), L), length(L,M), P is 1 / M, p(P)}. 3 | doc(X,Y,_) --> doc_neural(X,Y), {p(0.5)}. 4 | doc(X,Y,N) --> {N>0, N1 is N - 1, p(0.5)}, citep(X, X1), doc(X1,Y,N1). 5 | s(X) --> doc(X,Y,1), [Y]. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/citeseer/base/citeseer.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | from shutil import copy2 4 | import torch 5 | import numpy as np 6 | from torch.optim import Adam 7 | from time import time 8 | 9 | from deepstochlog.network import Network, NetworkStore 10 | from examples.citeseer.base.citeseer_data import train_dataset, valid_dataset, test_dataset, queries_for_model, citations 11 | from examples.citeseer.citeseer_utils import AccuracyCalculator 12 | from examples.models import Classifier 13 | from deepstochlog.utils import set_fixed_seed 14 | from deepstochlog.dataloader import DataLoader 15 | from deepstochlog.model import DeepStochLogModel 16 | from deepstochlog.trainer import DeepStochLogTrainer, print_logger, PrintFileLogger 17 | from deepstochlog.term import Term, List 18 | 19 | root_path = Path(__file__).parent 20 | 21 | 22 | class GreedyDumbEvaluation: 23 | def __init__(self, documents, labels, store): 24 | self.store = store 25 | self.documents = documents 26 | self.labels = labels 27 | 28 | def __call__(self): 29 | classifier = self.store.networks["classifier"].neural_model 30 | classifier.eval() 31 | 32 | acc = torch.mean((torch.argmax(classifier(self.documents),dim=1) == self.labels).float()) 33 | 34 | classifier.train() 35 | return "%s" % str(acc.numpy().tolist()) 36 | 37 | def run( 38 | epochs=100, 39 | batch_size=32, 40 | lr=0.01, 41 | expression_length=None, 42 | expression_max_length=3, 43 | allow_division=True, 44 | device_str: str = None, 45 | # 46 | train_size=None, 47 | test_size=None, 48 | # 49 | log_freq=50, 50 | logger=print_logger, 51 | test_example_idx=None, 52 | test_batch_size=100, 53 | # 54 | seed=None, 55 | verbose=True, 56 | ): 57 | 58 | 59 | 60 | 61 | set_fixed_seed(seed) 62 | 63 | # Load the MNIST model, and Adam optimiser 64 | input_size = len(train_dataset.documents[0]) 65 | classifier = Classifier(input_size=input_size) 66 | classifier_network = Network( 67 | "classifier", 68 | classifier, 69 | index_list=[Term(str(op)) for op in range(6)], 70 | ) 71 | networks = NetworkStore(classifier_network) 72 | 73 | if device_str is not None: 74 | device = torch.device(device_str) 75 | else: 76 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 77 | 78 | 79 | proving_start = time() 80 | model = DeepStochLogModel.from_file( 81 | file_location=str((root_path / "citeseer.pl").absolute()), 82 | query=queries_for_model, 83 | networks=networks, 84 | device=device, 85 | prolog_facts= citations, 86 | verbose=verbose 87 | ) 88 | proving_time = time() - proving_start 89 | 90 | optimizer = Adam(model.get_all_net_parameters(), lr=lr) 91 | optimizer.zero_grad() 92 | 93 | if verbose: 94 | logger.print("\nProving the program took {:.2f} seconds".format(proving_time)) 95 | 96 | 97 | 98 | # Own DataLoader that can deal with proof trees and tensors (replicates the pytorch dataloader interface) 99 | train_dataloader = DataLoader(train_dataset, batch_size=batch_size) 100 | 101 | # Create test functions 102 | # run_test_query = create_run_test_query_probability( 103 | # model, test_data, test_example_idx, verbose 104 | # ) 105 | calculate_model_accuracy = AccuracyCalculator( model=model, 106 | valid=valid_dataset, 107 | test=test_dataset, 108 | start_time=time()) 109 | # run_test_query = create_run_test_query(model, test_data, test_example_idx, verbose) 110 | # calculate_model_accuracy = create_model_accuracy_calculator(model, test_dataloader, start_time) 111 | # g = GreedyEvaluation(valid_data, test_data, networks) 112 | # calculate_model_accuracy = "Acc", GreedyEvaluation(documents, labels, networks) 113 | 114 | # Train the DeepStochLog model 115 | trainer = DeepStochLogTrainer( 116 | log_freq=log_freq, 117 | accuracy_tester=(calculate_model_accuracy.header, calculate_model_accuracy), 118 | logger=logger, 119 | print_time=verbose, 120 | ) 121 | trainer.train( 122 | model=model, 123 | optimizer=optimizer, 124 | dataloader=train_dataloader, 125 | epochs=epochs, 126 | ) 127 | 128 | return None 129 | 130 | 131 | if __name__ == "__main__": 132 | 133 | 134 | run( 135 | test_example_idx=0, 136 | expression_max_length=1, 137 | expression_length=1, 138 | epochs=1000, 139 | batch_size=len(train_dataset), 140 | seed=0, 141 | log_freq=1, 142 | allow_division=True, 143 | verbose=True, 144 | ) 145 | -------------------------------------------------------------------------------- /examples/citeseer/base/citeseer_data.py: -------------------------------------------------------------------------------- 1 | import dgl 2 | import numpy as np 3 | from pathlib import Path 4 | import torch 5 | from deepstochlog.term import Term, List 6 | from deepstochlog.context import ContextualizedTerm, Context 7 | from deepstochlog.dataset import ContextualizedTermDataset 8 | 9 | root_path = Path(__file__).parent 10 | 11 | 12 | 13 | dataset = dgl.data.CiteseerGraphDataset() 14 | g = dataset[0] 15 | 16 | # get node feature 17 | documents = g.ndata['feat'] 18 | # get data split 19 | train_ids = np.where(g.ndata['train_mask'].numpy())[0] 20 | val_ids = np.where(g.ndata['val_mask'].numpy())[0] 21 | test_ids = np.where(g.ndata['test_mask'].numpy())[0] 22 | 23 | # get labels 24 | labels = g.ndata['label'].numpy() 25 | 26 | edges = [] 27 | 28 | 29 | 30 | 31 | citations = [] 32 | for eid in range(g.num_edges()): 33 | a, b = g.find_edges(eid) 34 | a, b = a.numpy().tolist()[0], b.numpy().tolist()[0], 35 | edges.append((a,b)) 36 | citations.append("cite(%d, %d)." % (a,b)) 37 | citations = "\n".join(citations) 38 | 39 | 40 | 41 | def queries_from_ids(ids, labels, is_test = False): 42 | queries = [] 43 | 44 | 45 | 46 | 47 | class CiteseerDataset(ContextualizedTermDataset): 48 | def __init__( 49 | self, 50 | split: str, 51 | labels, 52 | documents): 53 | if split == "train": 54 | self.ids = train_ids 55 | elif split =="valid": 56 | self.ids = val_ids 57 | elif split == "test": 58 | self.ids = test_ids 59 | else: 60 | raise Exception("Unkonw split %s" % split) 61 | self.labels = labels 62 | self.is_test = True if split in ("test", "valid") else False 63 | self.documents = documents 64 | self.dataset = [] 65 | 66 | context = {Term(str(i)): d for i, d in enumerate(self.documents)} 67 | context = Context(context) 68 | self.queries_for_model = [] 69 | for did in self.ids: 70 | label = List(self.labels[did]) 71 | query = ContextualizedTerm( 72 | context=context, 73 | term=Term("s", did, label)) 74 | self.dataset.append(query) 75 | if self.is_test: 76 | query_model = Term("s", did, List("_")) 77 | else: 78 | query_model = query.term 79 | self.queries_for_model.append(query_model) 80 | 81 | def __len__(self): 82 | return len(self.dataset) 83 | 84 | def __getitem__(self, item): 85 | if type(item) is slice: 86 | return (self[i] for i in range(*item.indices(len(self)))) 87 | return self.dataset[item] 88 | 89 | 90 | 91 | train_dataset = CiteseerDataset(split="train", documents=documents, labels=labels) 92 | valid_dataset = CiteseerDataset(split="valid", documents=documents, labels=labels) 93 | test_dataset = CiteseerDataset(split="test", documents=documents, labels=labels) 94 | 95 | queries_for_model = train_dataset.queries_for_model + valid_dataset.queries_for_model + test_dataset.queries_for_model 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /examples/citeseer/citeseer_utils.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Callable, List, Dict 2 | from time import time 3 | from torch import nn 4 | 5 | import torch 6 | from deepstochlog.context import ContextualizedTerm 7 | from deepstochlog.dataloader import DataLoader 8 | from deepstochlog.utils import calculate_zipped_probabilities 9 | 10 | 11 | 12 | 13 | class AccuracyCalculator(): 14 | 15 | def __init__(self, model, valid, test, start_time, after_epoch = 0): 16 | self.model = model 17 | self.valid_set = valid 18 | self.test_set = test 19 | self.max_val = 0. 20 | self.current_test = 0. 21 | self.start_time = start_time 22 | self.header = "Valid acc\tTest acc\tP(correct)\ttime" 23 | self.after_epoch = after_epoch 24 | self.epoch = 0 25 | 26 | 27 | def __call__(self): 28 | 29 | self.epoch += 1 30 | if self.after_epoch< self.epoch: 31 | for network in self.model.neural_networks.networks.values(): 32 | network.neural_model.eval() 33 | acc, average_right_prob= calculate_accuracy(self.model, self.valid_set) 34 | if acc > self.max_val: 35 | self.max_val = acc 36 | self.current_test, _ = calculate_accuracy(self.model, self.test_set) 37 | for network in self.model.neural_networks.networks.values(): 38 | network.neural_model.train() 39 | else: 40 | acc = 0 41 | average_right_prob = 0 42 | 43 | return "{:.3f}\t\t{:.3f}\t\t{:.3f}\t\t{:.3f}".format(acc, self.current_test, average_right_prob,time() - self.start_time ) 44 | 45 | 46 | 47 | 48 | 49 | 50 | def create_model_accuracy_calculator( 51 | model, test_dataloader: DataLoader, start_time 52 | ) -> Tuple[str, Callable]: 53 | def calculate_model_accuracy() -> str: 54 | acc, average_right_prob = calculate_accuracy(model, test_dataloader) 55 | return "{:.3f}\t\t{:.3f}\t\t{:.3f}".format( 56 | acc, average_right_prob, time() - start_time 57 | ) 58 | 59 | return "Test acc\tP(correct)\ttime", calculate_model_accuracy 60 | 61 | 62 | 63 | def calculate_accuracy( 64 | model, 65 | test_data, 66 | ) -> Tuple[float, float]: 67 | if len(test_data) == 0: 68 | return 0, 0 69 | with torch.no_grad(): 70 | test_acc = 0 71 | label_probability = 0 72 | for i, elem in enumerate(test_data): 73 | query_with_variable = test_data.queries_for_model[i] 74 | 75 | other_possibilities = [ContextualizedTerm(context=elem.context, term=t) 76 | for t in model.get_direct_proof_possibilities(query_with_variable)] 77 | 78 | # Map all results to queryable dict 79 | probabilities = dict() 80 | for possibility, prob in calculate_zipped_probabilities(model, other_possibilities): 81 | probabilities[possibility] = prob 82 | 83 | # Sum the probability of the labels 84 | own_prob = probabilities[elem] 85 | label_probability += own_prob 86 | 87 | # Check if it is the highest prediction out of all other possibilities 88 | is_highest = not any( 89 | [ 90 | pos 91 | for pos in other_possibilities 92 | if probabilities[pos] > own_prob 93 | ] 94 | ) 95 | if is_highest: 96 | test_acc += 1 97 | 98 | return test_acc / len(test_data), label_probability / len(test_data) 99 | 100 | class RuleWeights(nn.Module): 101 | def __init__(self, num_classes: 6, num_rules: int = 2): 102 | super(RuleWeights, self).__init__() 103 | self.net = nn.Sequential( 104 | nn.Embedding(num_classes, 2), 105 | nn.Softmax(-1) 106 | ) 107 | def forward(self, x): 108 | x = x.view(-1) 109 | x = self.net(x) 110 | return x 111 | 112 | class Influence(nn.Module): 113 | def __init__(self, num_documents: int,): 114 | super(Influence, self).__init__() 115 | self.net = nn.Sequential( 116 | nn.Embedding(num_documents, 5) 117 | ) 118 | def forward(self, x,y,l): 119 | x = x.view(-1) 120 | embx = self.net(x) 121 | emby = self.net(l) 122 | mask = (l > 0).float() # compute the pad mask 123 | scores = torch.einsum("ij, ikj -> ik", embx, emby) 124 | masked_scores = torch.exp(scores) * mask 125 | scores = masked_scores / torch.sum(masked_scores, dim=-1, keepdim=True) # softmax only on the non padded elements 126 | res = scores[torch.arange(y.size(0)), y] 127 | return res 128 | 129 | 130 | class Classifier(nn.Module): 131 | def __init__( 132 | self, input_size: int, num_outputs: int = 10, with_softmax: bool = True 133 | ): 134 | super(Classifier, self).__init__() 135 | self.with_softmax = with_softmax 136 | self.input_size = input_size 137 | if with_softmax: 138 | self.softmax = nn.Softmax(1) 139 | self.net = nn.Sequential( 140 | nn.Linear(input_size, 50), nn.ReLU(), 141 | nn.Linear(50, num_outputs) 142 | ) 143 | 144 | def forward(self, x): 145 | x = x.view(-1, self.input_size) 146 | x = self.net(x) 147 | if self.with_softmax: 148 | x = self.softmax(x) 149 | return x 150 | 151 | 152 | def pretraining(x,y, model,optimizer, epochs = 100): 153 | 154 | loss = nn.CrossEntropyLoss() 155 | for e in range(epochs): 156 | o = model(x) 157 | output = loss(o, y) 158 | output.backward() 159 | optimizer.step() 160 | optimizer.zero_grad() 161 | -------------------------------------------------------------------------------- /examples/citeseer/with_influence/citeseer_data_withinfluence.py: -------------------------------------------------------------------------------- 1 | import dgl 2 | import numpy as np 3 | from pathlib import Path 4 | import torch 5 | from deepstochlog.term import Term, List 6 | from deepstochlog.context import ContextualizedTerm, Context 7 | from deepstochlog.dataset import ContextualizedTermDataset 8 | from collections import defaultdict 9 | 10 | root_path = Path(__file__).parent 11 | 12 | 13 | 14 | dataset = dgl.data.CiteseerGraphDataset() 15 | g = dataset[0] 16 | 17 | # get node feature 18 | documents = g.ndata['feat'] 19 | # get data split 20 | train_ids = np.where(g.ndata['train_mask'].numpy())[0] 21 | val_ids = np.where(g.ndata['val_mask'].numpy())[0] 22 | test_ids = np.where(g.ndata['test_mask'].numpy())[0] 23 | 24 | # get labels 25 | labels = g.ndata['label'].numpy() 26 | 27 | edges = [] 28 | 29 | 30 | 31 | 32 | citations = [] 33 | adjacency_list = defaultdict(list) 34 | for eid in range(g.num_edges()): 35 | a, b = g.find_edges(eid) 36 | a, b = a.numpy().tolist()[0], b.numpy().tolist()[0], 37 | edges.append((a,b)) 38 | citations.append("cite(%d, %d)." % (a,b)) 39 | adjacency_list[a].append(b) 40 | citations = "\n".join(citations) 41 | 42 | 43 | 44 | class CiteseerDataset(ContextualizedTermDataset): 45 | def __init__( 46 | self, 47 | split: str, 48 | labels, 49 | documents): 50 | if split == "train": 51 | self.ids = train_ids 52 | elif split =="valid": 53 | self.ids = val_ids 54 | elif split == "test": 55 | self.ids = test_ids 56 | else: 57 | raise Exception("Unkonw split %s" % split) 58 | self.labels = labels 59 | self.is_test = True if split in ("test", "valid") else False 60 | self.documents = documents 61 | self.dataset = [] 62 | 63 | context = {List(Term(str(i))): (d,) for i, d in enumerate(self.documents)} 64 | for i in range(6): 65 | context[List(Term("class" + str(i)))] = (torch.tensor([i]),) 66 | 67 | # for h in adjacency_list: 68 | # all = adjacency_list[h] 69 | # l = List(*[Term(str(i)) for i in all]) 70 | # for j, t in enumerate(all): 71 | # context[List(Term(str(h)), Term(str(t)), l)] = (self.documents[h], torch.tensor([j]), self.documents[all]) 72 | for h in adjacency_list: 73 | all = adjacency_list[h] 74 | l = List(*[Term(str(i)) for i in all]) 75 | for j, t in enumerate(all): 76 | context[List(Term(str(h)), Term(str(t)), l)] = ( 77 | torch.tensor([h]), torch.tensor([j]), torch.tensor(all)) 78 | 79 | context = Context(context) 80 | self.queries_for_model = [] 81 | for did in self.ids: 82 | label = List("class" + str(self.labels[did])) 83 | query = ContextualizedTerm( 84 | context=context, 85 | term=Term("s", did, label)) 86 | self.dataset.append(query) 87 | if self.is_test: 88 | query_model = Term("s", did, List("_")) 89 | else: 90 | query_model = query.term 91 | self.queries_for_model.append(query_model) 92 | 93 | def __len__(self): 94 | return len(self.dataset) 95 | 96 | def __getitem__(self, item): 97 | if type(item) is slice: 98 | return (self[i] for i in range(*item.indices(len(self)))) 99 | return self.dataset[item] 100 | 101 | 102 | 103 | train_dataset = CiteseerDataset(split="train", documents=documents, labels=labels) 104 | valid_dataset = CiteseerDataset(split="valid", documents=documents, labels=labels) 105 | test_dataset = CiteseerDataset(split="test", documents=documents, labels=labels) 106 | 107 | queries_for_model = train_dataset.queries_for_model + valid_dataset.queries_for_model + test_dataset.queries_for_model 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /examples/citeseer/with_influence/citeseer_influence.pl: -------------------------------------------------------------------------------- 1 | doc_neural(X,Y) --> [], {nn(classifier, [X], Y), domain(Y, [class0,class1,class2,class3,class4,class5])}. 2 | influence(X,Y) --> [], {cite(X,Y), findall(T, cite(X,T), L), nn(influence, [X,Y,L], 1)}. 3 | 4 | 5 | doc(X,Y,N) --> doc_switch(X,Y,N,Z), {nn(rule_weight, [Y], Z), domain(Z, [neural,cite])}. 6 | doc_switch(X,Y,_, neural) --> doc_neural(X,Y). 7 | doc_switch(X,Y,N, cite) --> {N>0, N1 is N - 1}, influence(X, X1), doc(X1,Y,N1). 8 | 9 | s(X) --> doc(X,Y,2), [Y]. 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/citeseer/with_influence/citeseer_influence.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | from shutil import copy2 4 | import torch 5 | import numpy as np 6 | from torch.optim import Adam 7 | from time import time 8 | 9 | from deepstochlog.network import Network, NetworkStore 10 | from examples.citeseer.with_influence.citeseer_data_withinfluence import train_dataset, valid_dataset, test_dataset, queries_for_model, citations 11 | from examples.citeseer.citeseer_utils import create_model_accuracy_calculator, Classifier, RuleWeights, Influence 12 | from deepstochlog.utils import set_fixed_seed 13 | from deepstochlog.dataloader import DataLoader 14 | from deepstochlog.model import DeepStochLogModel 15 | from deepstochlog.trainer import DeepStochLogTrainer, print_logger, PrintFileLogger 16 | from deepstochlog.term import Term, List 17 | 18 | root_path = Path(__file__).parent 19 | 20 | 21 | def run( 22 | epochs=100, 23 | batch_size=32, 24 | lr=0.01, 25 | expression_length=None, 26 | expression_max_length=3, 27 | allow_division=True, 28 | device_str: str = None, 29 | # 30 | train_size=None, 31 | test_size=None, 32 | # 33 | log_freq=50, 34 | logger=print_logger, 35 | test_example_idx=None, 36 | test_batch_size=100, 37 | # 38 | seed=None, 39 | verbose=False, 40 | ): 41 | 42 | 43 | 44 | 45 | set_fixed_seed(seed) 46 | 47 | # Load the MNIST model, and Adam optimiser 48 | input_size = len(train_dataset.documents[0]) 49 | classifier = Classifier(input_size=input_size) 50 | rule_weights = RuleWeights(num_rules=2, num_classes=6) 51 | influence = Influence(num_documents=len(train_dataset.documents)) 52 | classifier_network = Network( 53 | "classifier", 54 | classifier, 55 | index_list=[Term("class"+str(i)) for i in range(6)], 56 | ) 57 | rule_weights_network = Network( 58 | "rule_weight", 59 | rule_weights, 60 | index_list=[Term(str("neural")), Term(str("cite"))], 61 | ) 62 | influence_network = Network( 63 | "influence", 64 | influence, 65 | index_list=[Term(str(1))], 66 | ) 67 | networks = NetworkStore(classifier_network, rule_weights_network, influence_network) 68 | 69 | if device_str is not None: 70 | device = torch.device(device_str) 71 | else: 72 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 73 | 74 | 75 | proving_start = time() 76 | model = DeepStochLogModel.from_file( 77 | file_location=str((root_path / "citeseer_influence.pl").absolute()), 78 | query=queries_for_model, 79 | networks=networks, 80 | device=device, 81 | prolog_facts= citations 82 | ) 83 | optimizer = Adam(model.get_all_net_parameters(), lr=lr) 84 | optimizer.zero_grad() 85 | proving_time = time() - proving_start 86 | 87 | if verbose: 88 | logger.print("\nProving the program took {:.2f} seconds".format(proving_time)) 89 | 90 | 91 | 92 | # Own DataLoader that can deal with proof trees and tensors (replicates the pytorch dataloader interface) 93 | train_dataloader = DataLoader(train_dataset, batch_size=batch_size) 94 | 95 | # Create test functions 96 | # run_test_query = create_run_test_query_probability( 97 | # model, test_data, test_example_idx, verbose 98 | # ) 99 | calculate_model_accuracy = create_model_accuracy_calculator( model, test_dataset, time() ) 100 | # run_test_query = create_run_test_query(model, test_data, test_example_idx, verbose) 101 | # calculate_model_accuracy = create_model_accuracy_calculator(model, test_dataloader, start_time) 102 | # g = GreedyEvaluation(valid_data, test_data, networks) 103 | # calculate_model_accuracy = "Acc", GreedyEvaluation(documents, labels, networks) 104 | 105 | # Train the DeepStochLog model 106 | trainer = DeepStochLogTrainer( 107 | log_freq=log_freq, 108 | accuracy_tester=calculate_model_accuracy, 109 | logger=logger, 110 | print_time=verbose, 111 | ) 112 | trainer.train( 113 | model=model, 114 | optimizer=optimizer, 115 | dataloader=train_dataloader, 116 | epochs=epochs, 117 | ) 118 | 119 | return None 120 | 121 | 122 | if __name__ == "__main__": 123 | 124 | 125 | run( 126 | test_example_idx=0, 127 | expression_max_length=1, 128 | expression_length=1, 129 | epochs=1000, 130 | batch_size=len(train_dataset), 131 | seed=0, 132 | log_freq=1, 133 | allow_division=True, 134 | verbose=True, 135 | ) 136 | -------------------------------------------------------------------------------- /examples/citeseer/with_rule_weights/citeseer_data_withrules.py: -------------------------------------------------------------------------------- 1 | import dgl 2 | import numpy as np 3 | from pathlib import Path 4 | import torch 5 | from deepstochlog.term import Term, List 6 | from deepstochlog.context import ContextualizedTerm, Context 7 | from deepstochlog.dataset import ContextualizedTermDataset 8 | 9 | root_path = Path(__file__).parent 10 | 11 | 12 | 13 | dataset = dgl.data.CiteseerGraphDataset() 14 | g = dataset[0] 15 | 16 | # get node feature 17 | documents = g.ndata['feat'] 18 | # get data split 19 | train_ids = np.where(g.ndata['train_mask'].numpy())[0] 20 | val_ids = np.where(g.ndata['val_mask'].numpy())[0] 21 | test_ids = np.where(g.ndata['test_mask'].numpy())[0] 22 | 23 | # get labels 24 | labels = g.ndata['label'].numpy() 25 | 26 | edges = [] 27 | 28 | 29 | pretraining_data = documents[train_ids], torch.tensor(labels[train_ids]) 30 | 31 | 32 | 33 | 34 | citations = [] 35 | for eid in range(g.num_edges()): 36 | a, b = g.find_edges(eid) 37 | a, b = a.numpy().tolist()[0], b.numpy().tolist()[0], 38 | edges.append((a,b)) 39 | citations.append("cite(%d, %d)." % (a,b)) 40 | citations = "\n".join(citations) 41 | 42 | 43 | 44 | def queries_from_ids(ids, labels, is_test = False): 45 | queries = [] 46 | 47 | 48 | 49 | 50 | class CiteseerDataset(ContextualizedTermDataset): 51 | def __init__( 52 | self, 53 | split: str, 54 | labels, 55 | documents): 56 | if split == "train": 57 | self.ids = train_ids 58 | elif split =="valid": 59 | self.ids = val_ids 60 | elif split == "test": 61 | self.ids = test_ids 62 | else: 63 | raise Exception("Unkonw split %s" % split) 64 | self.labels = labels 65 | self.is_test = True if split in ("test", "valid") else False 66 | self.documents = documents 67 | self.dataset = [] 68 | 69 | context = {Term(str(i)): d for i, d in enumerate(self.documents)} 70 | for i in range(6): 71 | context[Term("class" + str(i))] = torch.tensor([i]) 72 | context = Context(context) 73 | self.queries_for_model = [] 74 | for did in self.ids: 75 | label = Term("class" + str(self.labels[did])) 76 | query = ContextualizedTerm( 77 | context=context, 78 | term=Term("s", label, List(did))) 79 | self.dataset.append(query) 80 | query_model = Term("s", Term("_"), List(did)) 81 | self.queries_for_model.append(query_model) 82 | 83 | def __len__(self): 84 | return len(self.dataset) 85 | 86 | def __getitem__(self, item): 87 | if type(item) is slice: 88 | return (self[i] for i in range(*item.indices(len(self)))) 89 | return self.dataset[item] 90 | 91 | 92 | 93 | train_dataset = CiteseerDataset(split="train", documents=documents, labels=labels) 94 | valid_dataset = CiteseerDataset(split="valid", documents=documents, labels=labels) 95 | test_dataset = CiteseerDataset(split="test", documents=documents, labels=labels) 96 | 97 | queries_for_model = train_dataset.queries_for_model + valid_dataset.queries_for_model + test_dataset.queries_for_model 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /examples/citeseer/with_rule_weights/citeseer_ruleweights.pl: -------------------------------------------------------------------------------- 1 | dom_class(Y) :- member(Y, [class0,class1,class2,class3,class4,class5]). 2 | neural_or_cite(Y) :- member(Y, [neural,cite]). 3 | P::citep(X,Y) --> [], {cite(X,Y), findall(T, cite(X,T), L), length(L,M), P is 1 / M}. 4 | nn(classifier, [X], Y, dom_class) :: doc_neural(X,Y) --> []. 5 | 0.5::doc(X,Y,N) --> doc_neural(X,Y). 6 | 0.5::doc(X,Y,N) --> {N>0, N1 is N - 1}, citep(X,X1), doc(X1,Y,N1). 7 | s(Y) --> {dom_class(Y)}, [X], doc(X,Y,2). 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/citeseer/with_rule_weights/citeseer_ruleweights.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | from shutil import copy2 4 | import torch 5 | import numpy as np 6 | from torch.optim import Adam 7 | from time import time 8 | 9 | from deepstochlog.network import Network, NetworkStore 10 | from examples.citeseer.with_rule_weights.citeseer_data_withrules import train_dataset, valid_dataset, test_dataset, queries_for_model, citations, pretraining_data 11 | from examples.citeseer.citeseer_utils import create_model_accuracy_calculator, Classifier, RuleWeights, AccuracyCalculator, pretraining 12 | from deepstochlog.utils import set_fixed_seed 13 | from deepstochlog.dataloader import DataLoader 14 | from deepstochlog.model import DeepStochLogModel 15 | from deepstochlog.trainer import DeepStochLogTrainer, print_logger, PrintFileLogger 16 | from deepstochlog.term import Term, List 17 | 18 | root_path = Path(__file__).parent 19 | 20 | 21 | def run( 22 | epochs=100, 23 | batch_size=32, 24 | lr=0.01, 25 | expression_length=None, 26 | expression_max_length=3, 27 | allow_division=True, 28 | device_str: str = None, 29 | # 30 | train_size=None, 31 | test_size=None, 32 | # 33 | log_freq=50, 34 | logger=print_logger, 35 | test_example_idx=None, 36 | test_batch_size=100, 37 | # 38 | seed=None, 39 | verbose=False, 40 | ): 41 | 42 | 43 | 44 | 45 | set_fixed_seed(seed) 46 | 47 | # Load the MNIST model, and Adam optimiser 48 | input_size = len(train_dataset.documents[0]) 49 | classifier = Classifier(input_size=input_size) 50 | rule_weights = RuleWeights(num_rules=2, num_classes=6) 51 | classifier_network = Network( 52 | "classifier", 53 | classifier, 54 | index_list=[Term("class"+str(i)) for i in range(6)], 55 | ) 56 | rule_weight = Network( 57 | "rule_weight", 58 | rule_weights, 59 | index_list=[Term(str("neural")), Term(str("cite"))], 60 | ) 61 | networks = NetworkStore(classifier_network, rule_weight) 62 | 63 | # x, y = pretraining_data 64 | # pretraining(x, y, classifier, optimizer, epochs=300) 65 | 66 | if device_str is not None: 67 | device = torch.device(device_str) 68 | else: 69 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 70 | 71 | 72 | proving_start = time() 73 | model = DeepStochLogModel.from_file( 74 | file_location=str((root_path / "citeseer_ruleweights.pl").absolute()), 75 | query=queries_for_model, 76 | networks=networks, 77 | device=device, 78 | prolog_facts= citations, 79 | normalization=DeepStochLogModel.FULL_NORM 80 | ) 81 | optimizer = Adam(model.get_all_net_parameters(), lr=lr) 82 | optimizer.zero_grad() 83 | proving_time = time() - proving_start 84 | 85 | if verbose: 86 | logger.print("\nProving the program took {:.2f} seconds".format(proving_time)) 87 | 88 | 89 | 90 | # Own DataLoader that can deal with proof trees and tensors (replicates the pytorch dataloader interface) 91 | train_dataloader = DataLoader(train_dataset, batch_size=batch_size) 92 | 93 | # Create test functions 94 | # run_test_query = create_run_test_query_probability( 95 | # model, test_data, test_example_idx, verbose 96 | # ) 97 | calculate_model_accuracy = AccuracyCalculator( model=model, 98 | valid=valid_dataset, 99 | test=test_dataset, 100 | start_time=time(),after_epoch=30) 101 | # run_test_query = create_run_test_query(model, test_data, test_example_idx, verbose) 102 | # calculate_model_accuracy = create_model_accuracy_calculator(model, test_dataloader, start_time) 103 | # g = GreedyEvaluation(valid_data, test_data, networks) 104 | # calculate_model_accuracy = "Acc", GreedyEvaluation(documents, labels, networks) 105 | 106 | # Train the DeepStochLog model 107 | 108 | 109 | 110 | trainer = DeepStochLogTrainer( 111 | log_freq=log_freq, 112 | accuracy_tester=(calculate_model_accuracy.header, calculate_model_accuracy), 113 | logger=logger, 114 | print_time=verbose, 115 | ) 116 | trainer.train( 117 | model=model, 118 | optimizer=optimizer, 119 | dataloader=train_dataloader, 120 | epochs=epochs, 121 | ) 122 | 123 | return None 124 | 125 | 126 | if __name__ == "__main__": 127 | 128 | 129 | run( 130 | test_example_idx=0, 131 | expression_max_length=1, 132 | expression_length=1, 133 | epochs=1000, 134 | batch_size=len(train_dataset), 135 | seed=0, 136 | log_freq=1, 137 | allow_division=True, 138 | verbose=True, 139 | ) 140 | -------------------------------------------------------------------------------- /examples/citeseer/with_rule_weights_xy/citeseer_data_withrules.py: -------------------------------------------------------------------------------- 1 | import dgl 2 | import numpy as np 3 | from pathlib import Path 4 | import torch 5 | from deepstochlog.term import Term, List 6 | from deepstochlog.context import ContextualizedTerm, Context 7 | from deepstochlog.dataset import ContextualizedTermDataset 8 | 9 | root_path = Path(__file__).parent 10 | 11 | 12 | 13 | dataset = dgl.data.CiteseerGraphDataset() 14 | g = dataset[0] 15 | 16 | # get node feature 17 | documents = g.ndata['feat'] 18 | # get data split 19 | train_ids = np.where(g.ndata['train_mask'].numpy())[0] 20 | val_ids = np.where(g.ndata['val_mask'].numpy())[0] 21 | test_ids = np.where(g.ndata['test_mask'].numpy())[0] 22 | 23 | # get labels 24 | labels = g.ndata['label'].numpy() 25 | 26 | edges = [] 27 | 28 | 29 | 30 | 31 | citations = [] 32 | for eid in range(g.num_edges()): 33 | a, b = g.find_edges(eid) 34 | a, b = a.numpy().tolist()[0], b.numpy().tolist()[0], 35 | edges.append((a,b)) 36 | citations.append("cite(%d, %d)." % (a,b)) 37 | citations = "\n".join(citations) 38 | 39 | 40 | 41 | def queries_from_ids(ids, labels, is_test = False): 42 | queries = [] 43 | 44 | 45 | 46 | 47 | class CiteseerDataset(ContextualizedTermDataset): 48 | def __init__( 49 | self, 50 | split: str, 51 | labels, 52 | documents): 53 | if split == "train": 54 | self.ids = train_ids 55 | elif split =="valid": 56 | self.ids = val_ids 57 | elif split == "test": 58 | self.ids = test_ids 59 | else: 60 | raise Exception("Unkonw split %s" % split) 61 | self.labels = labels 62 | self.is_test = True if split in ("test", "valid") else False 63 | self.documents = documents 64 | self.dataset = [] 65 | 66 | context = {Term(str(i)): d for i, d in enumerate(self.documents)} 67 | for i in range(6): 68 | context[Term("class" + str(i))] = torch.tensor([i]) 69 | context = Context(context) 70 | self.queries_for_model = [] 71 | for did in self.ids: 72 | label = List("class" + str(self.labels[did])) 73 | query = ContextualizedTerm( 74 | context=context, 75 | term=Term("s", did, label)) 76 | self.dataset.append(query) 77 | if self.is_test: 78 | query_model = Term("s", did, List("_")) 79 | else: 80 | query_model = query.term 81 | self.queries_for_model.append(query_model) 82 | 83 | def __len__(self): 84 | return len(self.dataset) 85 | 86 | def __getitem__(self, item): 87 | if type(item) is slice: 88 | return (self[i] for i in range(*item.indices(len(self)))) 89 | return self.dataset[item] 90 | 91 | 92 | 93 | train_dataset = CiteseerDataset(split="train", documents=documents, labels=labels) 94 | valid_dataset = CiteseerDataset(split="valid", documents=documents, labels=labels) 95 | test_dataset = CiteseerDataset(split="test", documents=documents, labels=labels) 96 | 97 | queries_for_model = train_dataset.queries_for_model + valid_dataset.queries_for_model + test_dataset.queries_for_model 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /examples/citeseer/with_rule_weights_xy/citeseer_ruleweights.pl: -------------------------------------------------------------------------------- 1 | doc_neural(X,Y) --> [], {nn(classifier, X, Y), domain(Y, [class0,class1,class2,class3,class4,class5])}. 2 | citep(X,Y) --> [], {cite(X,Y), findall(T, cite(X,T), L), length(L,M), P is 1 / M, p(P)}. 3 | 4 | 5 | doc(X,Y,N) --> doc_switch(X,Y,N,Z), {nn(rule_weight, Y, Z), domain(Z, [neural,cite])}. 6 | 7 | doc_switch(X,Y,_, neural) --> doc_neural(X,Y). 8 | doc_switch(X,Y,N, cite) --> {N>0, N1 is N - 1, domain(Z, [class0,class1,class2,class3,class4,class5]), member(Y, [class0,class1,class2,class3,class4,class5]), nn(xy_switch, Y, Z)}, citep(X, X1), doc(X1,Z,N1). 9 | 10 | s(X) --> doc(X,Y,1), [Y]. 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/citeseer/with_rule_weights_xy/citeseer_ruleweights.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | from shutil import copy2 4 | import torch 5 | import numpy as np 6 | from torch.optim import Adam 7 | from time import time 8 | 9 | from deepstochlog.network import Network, NetworkStore 10 | from examples.citeseer.with_rule_weights_xy.citeseer_data_withrules import train_dataset, valid_dataset, test_dataset, queries_for_model, citations 11 | from examples.citeseer.citeseer_utils import create_model_accuracy_calculator, Classifier, RuleWeights, AccuracyCalculator 12 | from deepstochlog.utils import set_fixed_seed 13 | from deepstochlog.dataloader import DataLoader 14 | from deepstochlog.model import DeepStochLogModel 15 | from deepstochlog.trainer import DeepStochLogTrainer, print_logger, PrintFileLogger 16 | from deepstochlog.term import Term, List 17 | 18 | root_path = Path(__file__).parent 19 | 20 | 21 | def run( 22 | epochs=100, 23 | batch_size=32, 24 | lr=0.01, 25 | expression_length=None, 26 | expression_max_length=3, 27 | allow_division=True, 28 | device_str: str = None, 29 | # 30 | train_size=None, 31 | test_size=None, 32 | # 33 | log_freq=50, 34 | logger=print_logger, 35 | test_example_idx=None, 36 | test_batch_size=100, 37 | # 38 | seed=None, 39 | verbose=False, 40 | ): 41 | 42 | 43 | 44 | 45 | set_fixed_seed(seed) 46 | 47 | # Load the MNIST model, and Adam optimiser 48 | input_size = len(train_dataset.documents[0]) 49 | classifier = Classifier(input_size=input_size) 50 | rule_weights = RuleWeights(num_rules=2, num_classes=6) 51 | xy_switch = RuleWeights(num_rules=6, num_classes=6) 52 | classifier_network = Network( 53 | "classifier", 54 | classifier, 55 | index_list=[Term("class"+str(i)) for i in range(6)], 56 | ) 57 | rule_weight = Network( 58 | "rule_weight", 59 | rule_weights, 60 | index_list=[Term(str("neural")), Term(str("cite"))], 61 | ) 62 | xy_switch = Network( 63 | "xy_switch", 64 | xy_switch, 65 | index_list=[Term("class"+str(i)) for i in range(6)], 66 | ) 67 | networks = NetworkStore(classifier_network, rule_weight,xy_switch) 68 | 69 | if device_str is not None: 70 | device = torch.device(device_str) 71 | else: 72 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 73 | 74 | 75 | proving_start = time() 76 | model = DeepStochLogModel.from_file( 77 | file_location=str((root_path / "citeseer_ruleweights.pl").absolute()), 78 | query=queries_for_model, 79 | networks=networks, 80 | device=device, 81 | prolog_facts= citations 82 | ) 83 | optimizer = Adam(model.get_all_net_parameters(), lr=lr) 84 | optimizer.zero_grad() 85 | proving_time = time() - proving_start 86 | 87 | if verbose: 88 | logger.print("\nProving the program took {:.2f} seconds".format(proving_time)) 89 | 90 | 91 | 92 | # Own DataLoader that can deal with proof trees and tensors (replicates the pytorch dataloader interface) 93 | train_dataloader = DataLoader(train_dataset, batch_size=batch_size) 94 | 95 | # Create test functions 96 | # run_test_query = create_run_test_query_probability( 97 | # model, test_data, test_example_idx, verbose 98 | # ) 99 | calculate_model_accuracy = AccuracyCalculator( model=model, 100 | valid=valid_dataset, 101 | test=test_dataset, 102 | start_time=time()) 103 | # run_test_query = create_run_test_query(model, test_data, test_example_idx, verbose) 104 | # calculate_model_accuracy = create_model_accuracy_calculator(model, test_dataloader, start_time) 105 | # g = GreedyEvaluation(valid_data, test_data, networks) 106 | # calculate_model_accuracy = "Acc", GreedyEvaluation(documents, labels, networks) 107 | 108 | # Train the DeepStochLog model 109 | trainer = DeepStochLogTrainer( 110 | log_freq=log_freq, 111 | accuracy_tester=(calculate_model_accuracy.header, calculate_model_accuracy), 112 | logger=logger, 113 | print_time=verbose, 114 | ) 115 | trainer.train( 116 | model=model, 117 | optimizer=optimizer, 118 | dataloader=train_dataloader, 119 | epochs=epochs, 120 | ) 121 | 122 | return None 123 | 124 | 125 | if __name__ == "__main__": 126 | 127 | 128 | run( 129 | test_example_idx=0, 130 | expression_max_length=1, 131 | expression_length=1, 132 | epochs=1000, 133 | batch_size=len(train_dataset), 134 | seed=0, 135 | log_freq=1, 136 | allow_division=True, 137 | verbose=True, 138 | ) 139 | -------------------------------------------------------------------------------- /examples/citeseer/with_structure_learning/citeseer_data_struct.py: -------------------------------------------------------------------------------- 1 | import dgl 2 | import numpy as np 3 | from pathlib import Path 4 | import torch 5 | from deepstochlog.term import Term, List 6 | from deepstochlog.context import ContextualizedTerm, Context 7 | from deepstochlog.dataset import ContextualizedTermDataset 8 | 9 | root_path = Path(__file__).parent 10 | 11 | 12 | 13 | dataset = dgl.data.CiteseerGraphDataset() 14 | g = dataset[0] 15 | 16 | # get node feature 17 | documents = g.ndata['feat'] 18 | # get data split 19 | train_ids = np.where(g.ndata['train_mask'].numpy())[0] 20 | val_ids = np.where(g.ndata['val_mask'].numpy())[0] 21 | test_ids = np.where(g.ndata['test_mask'].numpy())[0] 22 | 23 | # get labels 24 | labels = g.ndata['label'].numpy() 25 | 26 | edges = [] 27 | 28 | 29 | pretraining_data = documents[train_ids], torch.tensor(labels[train_ids]) 30 | 31 | 32 | 33 | 34 | citations = [] 35 | for eid in range(g.num_edges()): 36 | a, b = g.find_edges(eid) 37 | a, b = a.numpy().tolist()[0], b.numpy().tolist()[0], 38 | edges.append((a,b)) 39 | citations.append("cite(%d, %d)." % (a,b)) 40 | citations = "\n".join(citations) 41 | 42 | 43 | 44 | def queries_from_ids(ids, labels, is_test = False): 45 | queries = [] 46 | 47 | 48 | 49 | 50 | class CiteseerDataset(ContextualizedTermDataset): 51 | def __init__( 52 | self, 53 | split: str, 54 | labels, 55 | documents): 56 | if split == "train": 57 | self.ids = train_ids 58 | elif split =="valid": 59 | self.ids = val_ids 60 | elif split == "test": 61 | self.ids = test_ids 62 | else: 63 | raise Exception("Unkonw split %s" % split) 64 | self.labels = labels 65 | self.is_test = True if split in ("test", "valid") else False 66 | self.documents = documents 67 | self.dataset = [] 68 | 69 | context = {Term(str(i)): d for i, d in enumerate(self.documents)} 70 | for i in range(6): 71 | context[Term("class" + str(i))] = torch.tensor([i]) 72 | context = Context(context) 73 | self.queries_for_model = [] 74 | for did in self.ids: 75 | label = Term("class" + str(self.labels[did])) 76 | query = ContextualizedTerm( 77 | context=context, 78 | term=Term("s", label, List(did))) 79 | self.dataset.append(query) 80 | query_model = Term("s", Term("_"), List(did)) 81 | self.queries_for_model.append(query_model) 82 | 83 | def __len__(self): 84 | return len(self.dataset) 85 | 86 | def __getitem__(self, item): 87 | if type(item) is slice: 88 | return (self[i] for i in range(*item.indices(len(self)))) 89 | return self.dataset[item] 90 | 91 | 92 | 93 | train_dataset = CiteseerDataset(split="train", documents=documents, labels=labels) 94 | valid_dataset = CiteseerDataset(split="valid", documents=documents, labels=labels) 95 | test_dataset = CiteseerDataset(split="test", documents=documents, labels=labels) 96 | 97 | queries_for_model = train_dataset.queries_for_model + valid_dataset.queries_for_model + test_dataset.queries_for_model 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /examples/citeseer/with_structure_learning/citeseer_struct.pl: -------------------------------------------------------------------------------- 1 | neural(X,Y,_) --> [], {nn(classifier, [X], Y), domain(Y, [class0,class1,class2,class3,class4,class5])}. 2 | citep(X,Y,N) --> {N>0, N1 is N - 1, cite(X,X1)}, doc(X1,Y,N1). 3 | doc(X,Y,N) --> {member(Z, [neural(X,Y,N),citep(X,Y,N)]), nn(rule_weight, [Y], Z)}, Z. 4 | s(Y) --> [X], doc(X,Y,2). 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/citeseer/with_structure_learning/citeseer_struct.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | from shutil import copy2 4 | import torch 5 | import numpy as np 6 | from torch.optim import Adam 7 | from time import time 8 | 9 | from deepstochlog.network import Network, NetworkStore 10 | from examples.citeseer.with_structure_learning.citeseer_data_struct import train_dataset, valid_dataset, test_dataset, queries_for_model, citations, pretraining_data 11 | from examples.citeseer.citeseer_utils import create_model_accuracy_calculator, Classifier, RuleWeights, AccuracyCalculator, pretraining 12 | from deepstochlog.utils import set_fixed_seed 13 | from deepstochlog.dataloader import DataLoader 14 | from deepstochlog.model import DeepStochLogModel 15 | from deepstochlog.trainer import DeepStochLogTrainer, print_logger, PrintFileLogger 16 | from deepstochlog.term import Term, List 17 | 18 | root_path = Path(__file__).parent 19 | 20 | 21 | def run( 22 | epochs=100, 23 | batch_size=32, 24 | lr=0.01, 25 | expression_length=None, 26 | expression_max_length=3, 27 | allow_division=True, 28 | device_str: str = None, 29 | # 30 | train_size=None, 31 | test_size=None, 32 | # 33 | log_freq=50, 34 | logger=print_logger, 35 | test_example_idx=None, 36 | test_batch_size=100, 37 | # 38 | seed=None, 39 | verbose=False, 40 | ): 41 | 42 | 43 | 44 | 45 | set_fixed_seed(seed) 46 | 47 | # Load the MNIST model, and Adam optimiser 48 | input_size = len(train_dataset.documents[0]) 49 | classifier = Classifier(input_size=input_size) 50 | rule_weights = RuleWeights(num_rules=2, num_classes=6) 51 | classifier_network = Network( 52 | "classifier", 53 | classifier, 54 | index_list=[Term("class"+str(i)) for i in range(6)], 55 | ) 56 | rule_weight = Network( 57 | "rule_weight", 58 | rule_weights, 59 | index_list=[Term(str("neural")), Term(str("citep"))], 60 | ) 61 | networks = NetworkStore(classifier_network, rule_weight) 62 | 63 | # x, y = pretraining_data 64 | # pretraining(x, y, classifier, optimizer, epochs=300) 65 | 66 | if device_str is not None: 67 | device = torch.device(device_str) 68 | else: 69 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 70 | 71 | 72 | proving_start = time() 73 | model = DeepStochLogModel.from_file( 74 | file_location=str((root_path / "citeseer_struct.pl").absolute()), 75 | query=queries_for_model, 76 | networks=networks, 77 | device=device, 78 | prolog_facts= citations 79 | ) 80 | optimizer = Adam(model.get_all_net_parameters(), lr=lr) 81 | optimizer.zero_grad() 82 | proving_time = time() - proving_start 83 | 84 | if verbose: 85 | logger.print("\nProving the program took {:.2f} seconds".format(proving_time)) 86 | 87 | 88 | 89 | # Own DataLoader that can deal with proof trees and tensors (replicates the pytorch dataloader interface) 90 | train_dataloader = DataLoader(train_dataset, batch_size=batch_size) 91 | 92 | # Create test functions 93 | # run_test_query = create_run_test_query_probability( 94 | # model, test_data, test_example_idx, verbose 95 | # ) 96 | calculate_model_accuracy = AccuracyCalculator( model=model, 97 | valid=valid_dataset, 98 | test=test_dataset, 99 | start_time=time(),after_epoch=30) 100 | # run_test_query = create_run_test_query(model, test_data, test_example_idx, verbose) 101 | # calculate_model_accuracy = create_model_accuracy_calculator(model, test_dataloader, start_time) 102 | # g = GreedyEvaluation(valid_data, test_data, networks) 103 | # calculate_model_accuracy = "Acc", GreedyEvaluation(documents, labels, networks) 104 | 105 | # Train the DeepStochLog model 106 | 107 | 108 | 109 | trainer = DeepStochLogTrainer( 110 | log_freq=log_freq, 111 | accuracy_tester=(calculate_model_accuracy.header, calculate_model_accuracy), 112 | logger=logger, 113 | print_time=verbose, 114 | ) 115 | trainer.train( 116 | model=model, 117 | optimizer=optimizer, 118 | dataloader=train_dataloader, 119 | epochs=epochs, 120 | ) 121 | 122 | return None 123 | 124 | 125 | if __name__ == "__main__": 126 | 127 | 128 | run( 129 | test_example_idx=0, 130 | expression_max_length=1, 131 | expression_length=1, 132 | epochs=1000, 133 | batch_size=len(train_dataset), 134 | seed=0, 135 | log_freq=1, 136 | allow_division=True, 137 | verbose=True, 138 | ) 139 | -------------------------------------------------------------------------------- /examples/cora/with_rule_weights/cora_data_withrules.py: -------------------------------------------------------------------------------- 1 | import dgl 2 | import numpy as np 3 | from pathlib import Path 4 | import torch 5 | from deepstochlog.term import Term, List 6 | from deepstochlog.context import ContextualizedTerm, Context 7 | from deepstochlog.dataset import ContextualizedTermDataset 8 | 9 | root_path = Path(__file__).parent 10 | 11 | 12 | 13 | dataset = dgl.data.CoraGraphDataset() 14 | g = dataset[0] 15 | 16 | # get node feature 17 | documents = g.ndata['feat'] 18 | # get data split 19 | train_ids = np.where(g.ndata['train_mask'].numpy())[0] 20 | val_ids = np.where(g.ndata['val_mask'].numpy())[0] 21 | test_ids = np.where(g.ndata['test_mask'].numpy())[0] 22 | 23 | # get labels 24 | labels = g.ndata['label'].numpy() 25 | 26 | edges = [] 27 | 28 | 29 | 30 | 31 | citations = [] 32 | for eid in range(g.num_edges()): 33 | a, b = g.find_edges(eid) 34 | a, b = a.numpy().tolist()[0], b.numpy().tolist()[0], 35 | edges.append((a,b)) 36 | citations.append("cite(%d, %d)." % (a,b)) 37 | citations = "\n".join(citations) 38 | 39 | 40 | 41 | def queries_from_ids(ids, labels, is_test = False): 42 | queries = [] 43 | 44 | 45 | 46 | 47 | class CoraDataset(ContextualizedTermDataset): 48 | def __init__( 49 | self, 50 | split: str, 51 | labels, 52 | documents): 53 | if split == "train": 54 | self.ids = train_ids 55 | elif split =="valid": 56 | self.ids = val_ids 57 | elif split == "test": 58 | self.ids = test_ids 59 | else: 60 | raise Exception("Unkonw split %s" % split) 61 | self.labels = labels 62 | self.is_test = True if split in ("test", "valid") else False 63 | self.documents = documents 64 | self.dataset = [] 65 | 66 | context = {Term(str(i)): d for i, d in enumerate(self.documents)} 67 | for i in range(7): 68 | context[Term("class" + str(i))] = torch.tensor([i]) 69 | context = Context(context) 70 | self.queries_for_model = [] 71 | for did in self.ids: 72 | label = Term("class" + str(self.labels[did])) 73 | query = ContextualizedTerm( 74 | context=context, 75 | term=Term("s", label, List(did))) 76 | self.dataset.append(query) 77 | if self.is_test: 78 | query_model = Term("s", Term("_"), List(did)) 79 | else: 80 | query_model = query.term 81 | self.queries_for_model.append(query_model) 82 | 83 | def __len__(self): 84 | return len(self.dataset) 85 | 86 | def __getitem__(self, item): 87 | if type(item) is slice: 88 | return (self[i] for i in range(*item.indices(len(self)))) 89 | return self.dataset[item] 90 | 91 | 92 | 93 | train_dataset = CoraDataset(split="train", documents=documents, labels=labels) 94 | valid_dataset = CoraDataset(split="valid", documents=documents, labels=labels) 95 | test_dataset = CoraDataset(split="test", documents=documents, labels=labels) 96 | 97 | queries_for_model = train_dataset.queries_for_model + valid_dataset.queries_for_model + test_dataset.queries_for_model 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /examples/cora/with_rule_weights/cora_ruleweights.pl: -------------------------------------------------------------------------------- 1 | dom_class(X) :- member(X, [class0,class1,class2,class3,class4,class5,class6]). 2 | nn(classifier, [X], Y, dom_class) :: doc_neural(X,Y) --> []. 3 | 4 | citep(X,Y) --> [], {cite(X,Y), findall(T, cite(X,T), L), length(L,M), P is 1 / M, p(P)}. 5 | 6 | dom_rule_weight(X) :- member(X, [neural,cite]). 7 | nn(rule_weight, [Y], Z, dom_rule_weight) :: doc(X,Y,N) --> doc_switch(X,Y,N,Z). 8 | doc_switch(X,Y,_, neural) --> doc_neural(X,Y). 9 | doc_switch(X,Y,N, cite) --> {N>0, N1 is N - 1}, citep(X, X1), doc(X1,Y,N1). 10 | s(Y) --> [X], doc(X,Y,2). 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/cora/with_rule_weights/cora_ruleweights.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | from shutil import copy2 4 | import torch 5 | import numpy as np 6 | from torch.optim import Adam 7 | from time import time 8 | 9 | from deepstochlog.network import Network, NetworkStore 10 | from examples.cora.with_rule_weights.cora_data_withrules import train_dataset, valid_dataset, test_dataset, queries_for_model, citations 11 | from examples.citeseer.citeseer_utils import create_model_accuracy_calculator, Classifier, RuleWeights, AccuracyCalculator 12 | from deepstochlog.utils import set_fixed_seed 13 | from deepstochlog.dataloader import DataLoader 14 | from deepstochlog.model import DeepStochLogModel 15 | from deepstochlog.trainer import DeepStochLogTrainer, print_logger, PrintFileLogger 16 | from deepstochlog.term import Term, List 17 | 18 | root_path = Path(__file__).parent 19 | 20 | 21 | def run( 22 | epochs=100, 23 | batch_size=32, 24 | lr=0.01, 25 | expression_length=None, 26 | expression_max_length=3, 27 | allow_division=True, 28 | device_str: str = None, 29 | # 30 | train_size=None, 31 | test_size=None, 32 | # 33 | log_freq=50, 34 | logger=print_logger, 35 | test_example_idx=None, 36 | test_batch_size=100, 37 | # 38 | seed=None, 39 | verbose=False, 40 | ): 41 | 42 | 43 | 44 | 45 | set_fixed_seed(seed) 46 | 47 | # Load the MNIST model, and Adam optimiser 48 | input_size = len(train_dataset.documents[0]) 49 | classifier = Classifier(input_size=input_size) 50 | rule_weights = RuleWeights(num_rules=2, num_classes=7) 51 | classifier_network = Network( 52 | "classifier", 53 | classifier, 54 | index_list=[Term("class"+str(i)) for i in range(7)], 55 | ) 56 | rule_weight = Network( 57 | "rule_weight", 58 | rule_weights, 59 | index_list=[Term(str("neural")), Term(str("cite"))], 60 | ) 61 | networks = NetworkStore(classifier_network, rule_weight) 62 | 63 | if device_str is not None: 64 | device = torch.device(device_str) 65 | else: 66 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 67 | 68 | 69 | proving_start = time() 70 | model = DeepStochLogModel.from_file( 71 | file_location=str((root_path / "cora_ruleweights.pl").absolute()), 72 | query=queries_for_model, 73 | networks=networks, 74 | device=device, 75 | prolog_facts= citations, 76 | normalization=DeepStochLogModel.FULL_NORM 77 | ) 78 | optimizer = Adam(model.get_all_net_parameters(), lr=lr) 79 | optimizer.zero_grad() 80 | proving_time = time() - proving_start 81 | 82 | if verbose: 83 | logger.print("\nProving the program took {:.2f} seconds".format(proving_time)) 84 | 85 | 86 | 87 | # Own DataLoader that can deal with proof trees and tensors (replicates the pytorch dataloader interface) 88 | train_dataloader = DataLoader(train_dataset, batch_size=batch_size) 89 | 90 | # Create test functions 91 | # run_test_query = create_run_test_query_probability( 92 | # model, test_data, test_example_idx, verbose 93 | # ) 94 | calculate_model_accuracy = AccuracyCalculator( model=model, 95 | valid=valid_dataset, 96 | test=test_dataset, 97 | start_time=time()) # run_test_query = create_run_test_query(model, test_data, test_example_idx, verbose) 98 | # calculate_model_accuracy = create_model_accuracy_calculator(model, test_dataloader, start_time) 99 | # g = GreedyEvaluation(valid_data, test_data, networks) 100 | # calculate_model_accuracy = "Acc", GreedyEvaluation(documents, labels, networks) 101 | 102 | # Train the DeepStochLog model 103 | trainer = DeepStochLogTrainer( 104 | log_freq=log_freq, 105 | accuracy_tester=(calculate_model_accuracy.header, calculate_model_accuracy), 106 | logger=logger, 107 | print_time=verbose, 108 | ) 109 | trainer.train( 110 | model=model, 111 | optimizer=optimizer, 112 | dataloader=train_dataloader, 113 | epochs=epochs, 114 | ) 115 | 116 | return None 117 | 118 | 119 | if __name__ == "__main__": 120 | 121 | 122 | run( 123 | test_example_idx=0, 124 | expression_max_length=1, 125 | expression_length=1, 126 | epochs=200, 127 | batch_size=len(train_dataset), 128 | seed=0, 129 | log_freq=1, 130 | allow_division=True, 131 | verbose=True, 132 | ) 133 | -------------------------------------------------------------------------------- /examples/data_utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | import typing 3 | from pathlib import Path 4 | from typing import List, Tuple, Generator 5 | 6 | import torch 7 | import torchvision 8 | from torchvision import transforms as transforms 9 | from torchvision.datasets import MNIST 10 | 11 | transform = transforms.Compose( 12 | [transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))] 13 | ) 14 | data_root = Path(__file__).parent / ".." / "data" 15 | 16 | 17 | def get_mnist_data(train: bool) -> MNIST: 18 | return torchvision.datasets.MNIST( 19 | root=str(data_root / "raw/"), train=train, download=True, transform=transform 20 | ) 21 | 22 | 23 | def get_mnist_digits( 24 | train: bool, digits: List[int], output_names: bool = False 25 | ) -> Tuple[Generator, ...]: 26 | dataset = get_mnist_data(train) 27 | if not output_names: 28 | return tuple( 29 | ( 30 | dataset[i][0] 31 | for i in (dataset.targets == digit).nonzero(as_tuple=True)[0] 32 | ) 33 | for digit in digits 34 | ) 35 | prefix = "train_" if train else "test_" 36 | return tuple( 37 | (prefix + str(i.item()) for i in (dataset.targets == digit).nonzero(as_tuple=True)[0]) 38 | for digit in digits 39 | ) 40 | 41 | 42 | def split_train_dataset(dataset: typing.List, val_num_digits: int, train: bool): 43 | if train: 44 | return dataset[val_num_digits:] 45 | else: 46 | return dataset[:val_num_digits] 47 | 48 | 49 | def get_next(idx: int, elements: typing.MutableSequence) -> Tuple[any, int]: 50 | if idx >= len(elements): 51 | idx = 0 52 | random.shuffle(elements) 53 | result = elements[idx] 54 | idx += 1 55 | return result, idx -------------------------------------------------------------------------------- /examples/evaluate.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | from datetime import datetime 4 | import platform 5 | from pathlib import Path 6 | from time import time 7 | from typing import Callable, Iterable 8 | import scipy.stats as st 9 | 10 | import numpy 11 | import numpy as np 12 | from numpy import mean, std 13 | from pandas import DataFrame 14 | 15 | from deepstochlog.trainer import PandasLogger 16 | 17 | eval_root = Path(__file__).parent 18 | 19 | 20 | def create_default_parser(): 21 | parser = argparse.ArgumentParser() 22 | parser.add_argument("-e", "--epochs", type=int, default=5) 23 | parser.add_argument("-r", "--runs", type=int, default=5) 24 | parser.add_argument("-t", "--only_time", default=False, action="store_true") 25 | return parser 26 | 27 | 28 | def mean_confidence_interval(data: numpy.array, confidence=0.95): 29 | return st.t.interval( 30 | confidence, len(data) - 1, loc=np.mean(data), scale=st.sem(data) 31 | ) 32 | 33 | 34 | def pick_test_accuracy( 35 | df: DataFrame, maximize_attr: Iterable[str], target_attr: Iterable[str] 36 | ): 37 | new_df = df 38 | for attr in maximize_attr: 39 | new_df = new_df[new_df[attr].values == new_df[attr].values.max()] 40 | return [new_df[attr].iloc[-1] for attr in target_attr] 41 | 42 | 43 | def evaluate_deepstochlog_task( 44 | runner: Callable, 45 | arguments, 46 | runs=5, 47 | name=None, 48 | only_time=False, 49 | maximize_attr=("Val acc", "Val P(cor)"), 50 | target_attr=("Test acc", "Test P(cor)"), 51 | ): 52 | if only_time: 53 | print("Not measuring performance, only time to run.") 54 | maximize_attr = ("time",) 55 | target_attr = ("time",) 56 | arguments["val_size"] = 0 57 | arguments["test_size"] = 0 58 | arguments["log_freq"] = 100000 59 | if "val_number_digits" in arguments: 60 | arguments["val_number_digits"] = 0 61 | 62 | cur_time = str(time()) 63 | run_name = cur_time[: cur_time.index(".")] 64 | host_name = platform.node() 65 | folder = ( 66 | eval_root 67 | / ".." 68 | / "data" 69 | / "eval" 70 | / ( 71 | (("time-" if only_time else "") + name + "-" if name is not None else "") 72 | + run_name 73 | + "-" 74 | + host_name 75 | ) 76 | ) 77 | folder.mkdir(exist_ok=True, parents=True) 78 | 79 | test_accs = np.zeros((runs, len(target_attr))) 80 | outputs = [] 81 | 82 | for run in range(runs): 83 | logger = PandasLogger() 84 | 85 | output = runner(logger=logger, **arguments) 86 | outputs.append(output) 87 | if output and isinstance(output, dict) and "model" in output: 88 | del output["model"] 89 | 90 | df = logger.df 91 | print(df) 92 | 93 | test_acc = pick_test_accuracy( 94 | df=df, maximize_attr=maximize_attr, target_attr=target_attr 95 | ) 96 | for j in range(len(target_attr)): 97 | test_accs[run, j] = test_acc[j] 98 | 99 | # save the df 100 | csv = df.to_csv(index=False) 101 | with open(folder / "{}.csv".format(run), "w") as csv_file: 102 | csv_file.write(csv) 103 | 104 | results = { 105 | "host_name": host_name, 106 | "run_name": run_name, 107 | "date": str(datetime.now()), 108 | "runs": runs, 109 | "maximize_attr": maximize_attr, 110 | "target_attr": target_attr, 111 | "test_accs": test_accs.tolist(), 112 | "means": mean(test_accs, axis=0).tolist(), 113 | "standard deviatations": std(test_accs, axis=0).tolist(), 114 | "95% interval": [ 115 | mean_confidence_interval(test_accs[:, i], confidence=0.95) 116 | for i in range(len(target_attr)) 117 | ], 118 | "arguments": arguments, 119 | "outputs": outputs, 120 | } 121 | 122 | results_json = json.dumps(results, indent=4) 123 | 124 | print("Results:", results_json) 125 | with open(folder / "results.csv", "w") as results_file: 126 | results_file.write(results_json) 127 | -------------------------------------------------------------------------------- /examples/experiment_utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import gc 3 | 4 | 5 | def clear_all_lru_caches(): 6 | gc.collect() 7 | wrappers = [ 8 | a for a in gc.get_objects() if isinstance(a, functools._lru_cache_wrapper) 9 | ] 10 | 11 | for wrapper in wrappers: 12 | wrapper.cache_clear() 13 | -------------------------------------------------------------------------------- /examples/mathexpression/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ML-KULeuven/deepstochlog/aed95319411b8b5190b5b418b65b523b1436bcfd/examples/mathexpression/__init__.py -------------------------------------------------------------------------------- /examples/mathexpression/download_hwf.sh: -------------------------------------------------------------------------------- 1 | BASEDIR=$(dirname "$0") 2 | cd $BASEDIR 3 | cd .. 4 | cd .. 5 | cd data 6 | cd raw 7 | gdown 'https://drive.google.com/uc?id=1G07kw-wK-rqbg_85tuB7FNfA49q8lvoy' 8 | unzip HWF.zip 9 | rm -Rf pretrain-sym_net 10 | mv ../ ../ data/ hwf/ 11 | rm HWF.zip -------------------------------------------------------------------------------- /examples/mathexpression/mathexpression.pl: -------------------------------------------------------------------------------- 1 | dom_number(X) :- member(X, [0,1,2,3,4,5,6,7,8,9]). 2 | nn(number, [X], Y, dom_number) :: is_number(Y) --> [X]. 3 | 4 | dom_operator(X) :- member(X, [plus, minus, times, div]). 5 | nn(operator, [X], Y, dom_operator) :: operator(Y) --> [X]. 6 | factor(N) --> is_number(N). 7 | 8 | 0.34 :: term(N) --> factor(N). 9 | 0.33 :: term(N) --> term(N1), operator(times), factor(N2), {N is N1 * N2}. 10 | 0.33 :: term(N) --> term(N1), operator(div), factor(N2), {N2>0, N is N1 / N2}. 11 | 12 | 0.34 :: expression(N) --> term(N). 13 | 0.33 :: expression(N) --> expression(N1), operator(plus), term(N2), {N is N1 + N2}. 14 | 0.33 :: expression(N) --> expression(N1), operator(minus), term(N2), {N is N1 - N2}. -------------------------------------------------------------------------------- /examples/mathexpression/mathexpression_notabling.pl: -------------------------------------------------------------------------------- 1 | is_number(Y) --> [X], {domain(Y,[0,1,2,3,4,5,6,7,8,9]), nn(number, Y)}. 2 | operator(Y) --> [X], {domain(Y, [plus, minus, times, div]), nn(operator, Y)}. 3 | term(N) --> is_number(N), {p(0.34)}. 4 | term(N) --> is_number(N1), operator(times), term(N2), {N is N1 * N2, p(0.33)}. 5 | term(N) --> is_number(N1), operator(div), term(N2), {N2>0, N is N1 / N2,p(0.33)}. 6 | expression(N) --> term(N),{p(0.34)}. 7 | expression(N) --> term(N1), operator(plus), expression(N2), {N is N1 + N2, p(0.33)}. 8 | expression(N) --> term(N1), operator(minus), expression(N2), {N is N1 - N2, p(0.33)}. -------------------------------------------------------------------------------- /examples/mathexpression/mathexpression_with_probabilities.pl: -------------------------------------------------------------------------------- 1 | is_number(Y) --> [X], {domain(Y,[0,1,2,3,4,5,6,7,8,9]), nn(number,Y)}. 2 | operator(Y) --> [X], {domain(Y, [plus, minus, times, div]), nn(operator, Y)}. 3 | factor(N) --> is_number(N). 4 | term(N) --> term_switch(N,Y), {nn(term, Y), domain(Y, [0,1,2])}. 5 | term_switch(N, 0) --> factor(N). 6 | term_switch(N, 1) --> term(N1), operator(times), factor(N2), {N is N1 * N2}. 7 | term_switch(N, 2) --> term(N1), operator(div), factor(N2), {N2>0, N is N1 / N2}. 8 | expression(N) --> expression_switch(N,Y), {nn(expression, Y), domain(Y, [0,1,2])}. 9 | expression_switch(N,0) --> term(N). 10 | expression_switch(N,1) --> expression(N1), operator(plus), term(N2), {N is N1 + N2}. 11 | expression_switch(N,2) --> expression(N1), operator(minus), term(N2), {N is N1 - N2}. -------------------------------------------------------------------------------- /examples/models.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from torch import nn as nn 4 | import torch.nn.functional as F 5 | import torch 6 | 7 | 8 | class SymbolEncoder(nn.Module): 9 | def __init__(self): 10 | super(SymbolEncoder, self).__init__() 11 | self.convolutions = nn.Sequential( 12 | nn.Conv2d(1, 6, 3, stride=1, padding=1), 13 | nn.ReLU(), 14 | nn.MaxPool2d(2), 15 | nn.Conv2d(6, 16, 3, stride=1, padding=1), 16 | nn.ReLU(), 17 | nn.MaxPool2d(2), 18 | nn.Dropout2d(0.4), 19 | ) 20 | 21 | self.mlp = nn.Sequential( 22 | nn.Linear(16 * 11 * 11, 128), 23 | nn.ReLU(), 24 | # nn.Dropout2d(0.8) 25 | ) 26 | 27 | def forward(self, x): 28 | x = self.convolutions(x) 29 | x = torch.flatten(x, 1) 30 | x = self.mlp(x) 31 | return x 32 | 33 | 34 | class SymbolClassifier(nn.Module): 35 | def __init__(self, encoder, N=10): 36 | super(SymbolClassifier, self).__init__() 37 | self.encoder = encoder 38 | self.fc2 = nn.Linear(128, N) 39 | 40 | def forward(self, x): 41 | x = self.encoder(x) 42 | x = self.fc2(x) 43 | x = F.softmax(x, dim=-1) 44 | return x 45 | 46 | 47 | class MNISTNet(nn.Module): 48 | def __init__(self, output_features=10, with_softmax=True): 49 | super(MNISTNet, self).__init__() 50 | self.with_softmax = with_softmax 51 | if with_softmax: 52 | self.softmax = nn.Softmax(1) 53 | self.encoder = nn.Sequential( 54 | nn.Conv2d(1, 6, 5), 55 | nn.MaxPool2d(2, 2), # 6 24 24 -> 6 12 12 56 | nn.ReLU(True), 57 | nn.Conv2d(6, 16, 5), # 6 12 12 -> 16 8 8 58 | nn.MaxPool2d(2, 2), # 16 8 8 -> 16 4 4 59 | nn.ReLU(True), 60 | ) 61 | self.classifier = nn.Sequential( 62 | nn.Linear(16 * 4 * 4, 120), 63 | nn.ReLU(), 64 | nn.Linear(120, 84), 65 | nn.ReLU(), 66 | nn.Linear(84, output_features), 67 | ) 68 | 69 | def forward(self, x): 70 | x = self.encoder(x) 71 | x = x.view(-1, 16 * 4 * 4) 72 | x = self.classifier(x) 73 | if self.with_softmax: 74 | x = self.softmax(x) 75 | return x 76 | 77 | 78 | # Maybe later change with https://medium.com/@martinpella/how-to-use-pre-trained-word-embeddings-in-pytorch-71ca59249f76 79 | def get_word_vector_model(): 80 | import spacy 81 | 82 | # Load the spacy model that you have installed 83 | try: 84 | nlp = spacy.load("en_core_web_md") 85 | except IOError: 86 | # Word2Vec model is not loaded, let's load it! 87 | import os 88 | 89 | os.system("python -m spacy download en_core_web_md") 90 | nlp = spacy.load("en_core_web_md") 91 | return nlp 92 | 93 | 94 | def create_emb_layer(weights_matrix, non_trainable=False): 95 | num_embeddings, embedding_dim = weights_matrix.size() 96 | emb_layer = nn.Embedding(num_embeddings, embedding_dim) 97 | emb_layer.load_state_dict({"weight": weights_matrix}) 98 | if non_trainable: 99 | emb_layer.weight.requires_grad = False 100 | 101 | return emb_layer, num_embeddings, embedding_dim 102 | 103 | 104 | class EmbeddingsFFNet(nn.Module): 105 | def __init__(self): 106 | super().__init__() 107 | torch.manual_seed(0) 108 | self.net = nn.Sequential( 109 | nn.Linear(300, 200), 110 | nn.ReLU(), 111 | nn.Linear(200, 100), 112 | nn.ReLU(), 113 | nn.Linear(100, 2), 114 | nn.Softmax(dim=1), 115 | ) 116 | 117 | def forward(self, X): 118 | return self.net(X) 119 | 120 | def predict(self, X): 121 | Y_pred = self.forward(X) 122 | y_pred = torch.max(Y_pred, 1) 123 | predicted_labels = y_pred.indices 124 | return predicted_labels 125 | 126 | 127 | # def get_pretrained_model(path: str): 128 | # model = MNISTNet() 129 | # model.load_state_dict(torch.load(path)) 130 | # return model 131 | # 132 | # 133 | class Classifier(nn.Module): 134 | def __init__( 135 | self, input_size: int, num_outputs: int = 10, with_softmax: bool = True 136 | ): 137 | super(Classifier, self).__init__() 138 | self.with_softmax = with_softmax 139 | self.input_size = input_size 140 | if with_softmax: 141 | self.softmax = nn.Softmax(1) 142 | self.net = nn.Sequential( 143 | nn.Linear(input_size, 100), 144 | nn.ReLU(), 145 | nn.Linear(100, 100), 146 | nn.ReLU(), 147 | nn.Linear(100, num_outputs), 148 | ) 149 | 150 | def forward(self, x): 151 | x = x.view(-1, self.input_size) 152 | x = self.net(x) 153 | if self.with_softmax: 154 | x = self.softmax(x) 155 | return x 156 | 157 | 158 | class MLP(nn.Module): 159 | def __init__( 160 | self, 161 | *sizes, 162 | encoder=nn.Identity(), 163 | activation=nn.ReLU, 164 | softmax=True, 165 | batch=True 166 | ): 167 | super(MLP, self).__init__() 168 | layers = [] 169 | self.batch = batch 170 | for i in range(len(sizes) - 2): 171 | layers.append(nn.Linear(sizes[i], sizes[i + 1])) 172 | layers.append(activation()) 173 | layers.append(nn.Linear(sizes[-2], sizes[-1])) 174 | if softmax: 175 | layers.append(nn.Softmax(-1)) 176 | self.nn = nn.Sequential(*layers) 177 | self.encoder = encoder 178 | 179 | def forward(self, x): 180 | if not self.batch: 181 | x = x.unsqueeze(0) 182 | x = self.encoder(x) 183 | x = self.nn(x) 184 | return x 185 | 186 | 187 | class ImageEncoder(nn.Module): 188 | def __init__(self, output_features=10, with_softmax=True): 189 | super(ImageEncoder, self).__init__() 190 | self.with_softmax = with_softmax 191 | if with_softmax: 192 | self.softmax = nn.Softmax(1) 193 | self.conv_encoder = nn.Sequential( 194 | nn.Conv2d(1, 6, 5), 195 | nn.MaxPool2d(2, 2), # 6 24 24 -> 6 12 12 196 | nn.ReLU(True), 197 | nn.Conv2d(6, 16, 5), # 6 12 12 -> 16 8 8 198 | nn.MaxPool2d(2, 2), # 16 8 8 -> 16 4 4 199 | nn.ReLU(True), 200 | ) 201 | self.linear_encoder = nn.Sequential( 202 | nn.Linear(16 * 4 * 4, 120), 203 | nn.ReLU(), 204 | nn.Linear(120, 84), 205 | nn.ReLU(), 206 | nn.Linear(84, output_features), 207 | ) 208 | 209 | def forward(self, x): 210 | x = self.conv_encoder(x) 211 | x = x.view(-1, 16 * 4 * 4) 212 | x = self.linear_encoder(x) 213 | return x 214 | 215 | 216 | class LSTMSequenceImage(nn.Module): 217 | """Example usage: 218 | 219 | A = [torch.rand(l, 1, 28, 28) for l in [10,2,7]] # 3 sequences of 10,2,7 elements 220 | image_encoder_size = 50 221 | rnn_hidden_size = 100 222 | image_encoder = ImageEncoder(output_features = image_encoder_size) 223 | output = LSTMSequenceImage(image_encoder,image_encoder_size,rnn_hidden_size)(A) # tensor with 3 values in [0,1] 224 | 225 | """ 226 | 227 | def __init__( 228 | self, image_encoder, encoder_output_size: int, rnn_hidden_size=10 # MnistModel 229 | ): 230 | super().__init__() 231 | 232 | self.encoder_output_size = encoder_output_size 233 | self.image_encoder = image_encoder 234 | self.rnn_hidden_size = rnn_hidden_size 235 | self.rnn = nn.LSTM( 236 | input_size=self.encoder_output_size, hidden_size=self.rnn_hidden_size 237 | ) 238 | self.fc = nn.Sequential(nn.Linear(self.rnn_hidden_size, 1), nn.Sigmoid()) 239 | 240 | def forward(self, input_sequences: List[torch.Tensor]): 241 | batch_size = len(input_sequences) 242 | 243 | # Keep track if the indices of the last element of the sequences before pad 244 | last_indices = [len(i) - 1 for i in input_sequences] 245 | 246 | # Padding the sequence 247 | neural_input = torch.nn.utils.rnn.pad_sequence( 248 | input_sequences, batch_first=True 249 | ) 250 | 251 | # Reshaping to a 4-dimensional tensor (as required by MNISTNet() 252 | first_two = neural_input.shape[:2] 253 | neural_input = neural_input.view(-1, 1, 28, 28) 254 | 255 | # Encoding images in the sequence 256 | embedded_sequence = self.image_encoder(neural_input) 257 | 258 | # Restoring batch and sequence dimensions 259 | embedded_sequence = embedded_sequence.view(*(list(first_two) + [-1])) 260 | 261 | # Processing sequences with RNNs 262 | outputs, _ = self.rnn(embedded_sequence) 263 | 264 | # Taking the output for the last element of each sequence 265 | outputs = outputs[torch.arange(batch_size), last_indices] 266 | 267 | # Project it with a Linear layer with 1 output and sigmoid on top 268 | y = self.fc(outputs).squeeze(-1) 269 | return y 270 | -------------------------------------------------------------------------------- /examples/wap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ML-KULeuven/deepstochlog/aed95319411b8b5190b5b418b65b523b1436bcfd/examples/wap/__init__.py -------------------------------------------------------------------------------- /examples/wap/wap.pl: -------------------------------------------------------------------------------- 1 | dom_permute(X) :- member(X, [0,1,2,3,4,5]). 2 | dom_op1(X) :- member(X, [plus,minus,times,div]). 3 | dom_swap(X) :- member(X,[no_swap,swap]). 4 | dom_op2(X) :- member(X, [plus,minus,times,div]). 5 | 6 | nn(nn_permute, [Embed, Perm], Perm, dom_permute) :: nn_permute(Embed,Perm) --> []. 7 | nn(nn_op1, [Embed], Op1, dom_op1) :: nn_op1(Embed, Op1) --> []. 8 | nn(nn_swap, [Embed], Swap, dom_swap) :: nn_swap(Embed, Swap) --> []. 9 | nn(nn_op2, [Embed], Op2, dom_op2) :: nn_op2(Embed, Op2) --> []. 10 | 11 | 0.1666666 :: permute(0,A,B,C,A,B,C) --> []. 12 | 0.1666666 :: permute(1,A,B,C,A,C,B) --> []. 13 | 0.1666666 :: permute(2,A,B,C,B,A,C) --> []. 14 | 0.1666666 :: permute(3,A,B,C,B,C,A) --> []. 15 | 0.1666666 :: permute(4,A,B,C,C,A,B) --> []. 16 | 0.1666666 :: permute(5,A,B,C,C,B,A) --> []. 17 | 18 | 0.5 :: swap(no_swap,X,Y,X,Y) --> []. 19 | 0.5 :: swap(swap,X,Y,Y,X) --> []. 20 | 21 | 0.25 :: operator(plus,X,Y,Z) --> [], {Z is X+Y}. 22 | 0.25 :: operator(minus,X,Y,Z) --> [], {Z is X-Y}. 23 | 0.25 :: operator(times,X,Y,Z) --> [], {Z is X*Y}. 24 | 0.25 :: operator(div,X,Y,Z) --> [], {Y > 0, 0 =:= X mod Y, Z is X//Y}. 25 | 26 | s(Out,X1,X2,X3) --> [String], 27 | nn_permute(String, Perm), 28 | nn_op1(String, Op1), 29 | nn_swap(String, Swap), 30 | nn_op2(String, Op2), 31 | permute(Perm,X1,X2,X3,N1,N2,N3), 32 | operator(Op1,N1,N2,Res1), 33 | swap(Swap,Res1,N3,X,Y), 34 | operator(Op2,X,Y,Out). -------------------------------------------------------------------------------- /examples/wap/wap.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import torch 4 | from torch.optim import Adam 5 | from time import time 6 | 7 | from deepstochlog.network import Network, NetworkStore 8 | from deepstochlog.utils import ( 9 | set_fixed_seed, 10 | create_run_test_query, 11 | create_model_accuracy_calculator, 12 | ) 13 | from deepstochlog.dataloader import DataLoader 14 | from deepstochlog.model import DeepStochLogModel 15 | from deepstochlog.term import Term, List 16 | from deepstochlog.trainer import DeepStochLogTrainer, print_logger 17 | from examples.wap.wap_data import WapDataset 18 | from examples.wap.wap_network import RNN, vocab 19 | from examples.models import MLP 20 | 21 | root_path = Path(__file__).parent 22 | 23 | 24 | def run( 25 | # 26 | epochs=2, 27 | batch_size=32, 28 | lr=1e-3, 29 | hidden_size=512, 30 | n=8, 31 | p_drop=0.5, 32 | # 33 | train_size=None, 34 | val_size=None, 35 | test_size=None, 36 | # 37 | log_freq=50, 38 | logger=print_logger, 39 | test_example_idx=None, 40 | test_batch_size=100, 41 | val_batch_size=None, 42 | # 43 | seed=None, 44 | verbose=True, 45 | verbose_building=False, 46 | ): 47 | start_time = time() 48 | # Setting seed for reproducibility 49 | set_fixed_seed(seed) 50 | 51 | # Load the MNIST model, and Adam optimiser 52 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 53 | 54 | operators = [Term(e) for e in ["plus", "minus", "times", "div"]] 55 | 56 | rnn_encoder = RNN(len(vocab), hidden_size, device=device, p_drop=p_drop) 57 | nn_permute = Network( 58 | "nn_permute", 59 | MLP(hidden_size * n, 6, encoder=rnn_encoder), 60 | index_list=[Term(str(e)) for e in range(6)], 61 | concat_tensor_input=False, 62 | ) 63 | nn_op1 = Network( 64 | "nn_op1", 65 | MLP(hidden_size * n, 4, encoder=rnn_encoder), 66 | index_list=operators, 67 | concat_tensor_input=False, 68 | ) 69 | nn_swap = Network( 70 | "nn_swap", 71 | MLP(hidden_size * n, 2, encoder=rnn_encoder), 72 | index_list=[Term(e) for e in ["no_swap", "swap"]], 73 | concat_tensor_input=False, 74 | ) 75 | nn_op2 = Network( 76 | "nn_op2", 77 | MLP(hidden_size * n, 4, encoder=rnn_encoder), 78 | index_list=operators, 79 | concat_tensor_input=False, 80 | ) 81 | 82 | networks = NetworkStore(nn_permute, nn_op1, nn_swap, nn_op2) 83 | 84 | train_data = WapDataset( 85 | split="train", 86 | size=train_size, 87 | ) 88 | val_data = WapDataset( 89 | split="dev", 90 | size=val_size, 91 | ) 92 | test_data = WapDataset( 93 | split="test", 94 | size=test_size, 95 | ) 96 | 97 | # Own DataLoader that can deal with proof trees and tensors (replicates the pytorch dataloader interface) 98 | train_dataloader = DataLoader( 99 | train_data, 100 | batch_size=batch_size, 101 | ) 102 | val_dataloader = DataLoader( 103 | val_data, batch_size=val_batch_size if val_batch_size else test_batch_size 104 | ) 105 | test_dataloader = DataLoader(test_data, batch_size=test_batch_size) 106 | 107 | queries = train_data.calculate_queries( 108 | masked_generation_output=False 109 | ) | test_data.calculate_queries(masked_generation_output=True) 110 | 111 | grounding_start_time = time() 112 | model = DeepStochLogModel.from_file( 113 | file_location=str((root_path / "wap.pl").absolute()), 114 | query=queries, 115 | networks=networks, 116 | device=device, 117 | verbose=verbose_building, 118 | ) 119 | optimizer = Adam(model.get_all_net_parameters(), lr=lr) 120 | optimizer.zero_grad() 121 | grounding_time = time() - grounding_start_time 122 | if verbose: 123 | print("Grounding the program took {:.3} seconds".format(grounding_time)) 124 | 125 | run_test_query = create_run_test_query( 126 | model=model, 127 | test_data=test_data, 128 | test_example_idx=test_example_idx, 129 | verbose=verbose, 130 | parse_is_nnleaf_outputs=True, 131 | ) 132 | calculate_model_accuracy = create_model_accuracy_calculator( 133 | model=model, 134 | test_dataloader=test_dataloader, 135 | start_time=start_time, 136 | val_dataloader=val_dataloader, 137 | most_probable_parse_accuracy=False, 138 | ) 139 | 140 | # Train the DeepStochLog model 141 | trainer = DeepStochLogTrainer( 142 | log_freq=log_freq, 143 | accuracy_tester=calculate_model_accuracy, 144 | logger=logger, 145 | test_query=run_test_query, 146 | print_time=verbose, 147 | ) 148 | train_time = trainer.train( 149 | model=model, optimizer=optimizer, dataloader=train_dataloader, epochs=epochs 150 | ) 151 | 152 | return { 153 | "grounding_time": grounding_time, 154 | "training_time": train_time, 155 | "model": model, 156 | } 157 | 158 | 159 | if __name__ == "__main__": 160 | run( 161 | epochs=40, 162 | test_example_idx=[0, 1, 2, 3], 163 | # train_size=4, 164 | # val_size=4, 165 | # test_size=4, 166 | seed=42, 167 | log_freq=10, 168 | # verbose_building=False, 169 | ) 170 | -------------------------------------------------------------------------------- /examples/wap/wap_data.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections.abc import Sequence 3 | from pathlib import Path 4 | from typing import Union 5 | 6 | import typing 7 | 8 | from deepstochlog.dataset import ContextualizedTermDataset 9 | from deepstochlog.context import ContextualizedTerm, Context 10 | from deepstochlog.term import Term, List 11 | 12 | data_root = Path(__file__).parent / ".." / ".." / "data" / "raw" / "wap" 13 | 14 | 15 | class WapDataset(ContextualizedTermDataset): 16 | def __init__( 17 | self, 18 | split: str = "train", 19 | size: int = None, 20 | ): 21 | with open(data_root / "questions.json", "r") as questions_file: 22 | all_questions: typing.Dict = json.load(questions_file) 23 | with open(data_root / (split + ".txt"), "r") as split_file: 24 | question_answers: typing.List[typing.Tuple[int, str]] = [ 25 | (int(float(el[0])), el[1]) 26 | for el in [s.split("\t") for s in split_file.readlines()] 27 | ] 28 | 29 | # for i, q in enumerate(all_questions): 30 | # assert i == q["iIndex"] 31 | 32 | with open(data_root / (split + ".tsv")) as ids_file: 33 | idxs = [int(idx) for idx in ids_file.readlines()] 34 | questions = [ 35 | { 36 | **all_questions[idx], 37 | "tokenized_question": question_answers[i][1], 38 | } 39 | for i, idx in enumerate(idxs) 40 | ] 41 | 42 | if size is None: 43 | size = len(questions) 44 | 45 | self.ct_term_dataset = [] 46 | for idx in range(0, size): 47 | question = questions[idx] 48 | 49 | example = create_term(question) 50 | self.ct_term_dataset.append(example) 51 | 52 | def __len__(self): 53 | return len(self.ct_term_dataset) 54 | 55 | def __getitem__(self, item: Union[int, slice]): 56 | if type(item) is slice: 57 | return (self[i] for i in range(*item.indices(len(self)))) 58 | return self.ct_term_dataset[item] 59 | 60 | 61 | def get_number(question: str, alignment: int): 62 | number_str = "" 63 | while question[alignment].isdigit(): 64 | number_str += question[alignment] 65 | alignment += 1 66 | return int(number_str) 67 | 68 | 69 | def get_numbers(question: str, alignments: typing.List[int]): 70 | return tuple(get_number(question, alignment) for alignment in alignments) 71 | 72 | 73 | sentence_token = List(Term("sentence_token")) 74 | 75 | 76 | def create_term(question: typing.Dict) -> ContextualizedTerm: 77 | 78 | number1, number2, number3 = get_numbers( 79 | question["sQuestion"], question["lAlignments"] 80 | ) 81 | 82 | correct_sequence = question["lEquations"][0] 83 | # Remove "X=(" and ")", and then replace all ".0" from numbers 84 | correct_sequence_fixed = correct_sequence[3:-1].replace(".0","") 85 | 86 | return ContextualizedTerm( 87 | context=Context( 88 | {Term("sentence_token"): question["tokenized_question"]}, 89 | map_default_to_term=True, 90 | ), 91 | term=Term( 92 | "s", 93 | Term(str(int(question["lSolutions"][0]))), 94 | Term(str(number1)), 95 | Term(str(number2)), 96 | Term(str(number3)), 97 | sentence_token, 98 | ), 99 | meta=correct_sequence, 100 | ) 101 | -------------------------------------------------------------------------------- /examples/wap/wap_evaluate.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | from examples.wap import wap 5 | from examples.evaluate import evaluate_deepstochlog_task 6 | 7 | 8 | eval_root = Path(__file__).parent 9 | 10 | 11 | def main(): 12 | arguments = { 13 | "train_size": None, 14 | "val_size": None, 15 | "test_size": None, 16 | "epochs": 40, 17 | "log_freq": 30, 18 | "test_example_idx": [], 19 | "verbose": False, 20 | } 21 | 22 | evaluate_deepstochlog_task( 23 | runner=wap.run, 24 | arguments=arguments, 25 | runs=5, 26 | name="wap", 27 | maximize_attr=("Val acc", "Val P(cor)"), 28 | target_attr=("Test acc", "Test P(cor)", "time"), 29 | ) 30 | 31 | 32 | if __name__ == "__main__": 33 | main() 34 | -------------------------------------------------------------------------------- /examples/wap/wap_network.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List 3 | 4 | import torch 5 | import torch.nn as nn 6 | 7 | embed_size = 256 8 | 9 | 10 | vocab_location = ( 11 | Path(__file__).parent / ".." / ".." / "data" / "raw" / "wap" / "vocab_746.txt" 12 | ) 13 | vocab = dict() 14 | with open(vocab_location) as f: 15 | for i, word in enumerate(f): 16 | word = word.strip() 17 | vocab[word] = i 18 | 19 | 20 | def tokenize(sentence): 21 | sentence = sentence.split(" ") 22 | tokens = [] 23 | numbers = list() 24 | indices = list() 25 | for i, word in enumerate(sentence): 26 | if word.isdigit(): 27 | numbers.append(int(word)) 28 | tokens.append("") 29 | indices.append(i) 30 | else: 31 | if word in vocab: 32 | tokens.append(word) 33 | else: 34 | tokens.append("") 35 | return [vocab[token] for token in tokens], numbers, indices 36 | 37 | 38 | class RNN(nn.Module): 39 | def __init__(self, vocab_size, hidden_size, device=None, p_drop=0.0): 40 | super(RNN, self).__init__() 41 | self.lstm = nn.GRU( 42 | embed_size, hidden_size, 1, bidirectional=True, batch_first=True 43 | ) 44 | self.hidden_size = hidden_size 45 | self.embedding = nn.Embedding( 46 | vocab_size, embed_size 47 | ) # , _weight=torch.nn.Parameter(weights)) 48 | self.dropout = nn.Dropout(p_drop) 49 | self.device = device 50 | 51 | def forward(self, sentence_input: List[List[str]]): 52 | 53 | tokenizations = [tokenize(s[0].strip('"').strip()) for s in sentence_input] 54 | 55 | # TODO: Implement this as batch instead 56 | tensors = [] 57 | for tokenization in tokenizations: 58 | x, _, indices = tokenization 59 | n1, n2, n3 = indices 60 | seq_len = len(x) 61 | x = torch.LongTensor(x).unsqueeze(0).to(self.device) 62 | x = self.embedding(x) 63 | x, _ = self.lstm(x) 64 | x = x.view(seq_len, 2, self.hidden_size) 65 | x1 = torch.cat([x[-1, 0, ...], x[n1, 0, ...], x[n2, 0, ...], x[n3, 0, ...]]) 66 | x2 = torch.cat([x[0, 1, ...], x[n1, 1, ...], x[n2, 1, ...], x[n3, 1, ...]]) 67 | x = torch.cat([x1, x2]) 68 | # return x 69 | tensors.append(self.dropout(x)) 70 | 71 | result = torch.stack(tensors) 72 | if self.device: 73 | result = result.to(self.device) 74 | 75 | return result 76 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | torch 2 | torchvision 3 | numpy 4 | pandas 5 | pyparsing 6 | dgl 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="deepstochlog", 8 | version="0.0.1", 9 | author="Thomas Winters, Giuseppe Marra, Robin Manhaeve, Luc De Raedt", 10 | author_email="firstname.lastname@kuleuven.be", 11 | description="Neural Stochastic Logic Programming", 12 | licence="Apache License 2.0", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | packages=setuptools.find_packages(include=["deepstochlog", "deepstochlog.*"]), 16 | python_requires='>=3', 17 | package_data={'deepstochlog': ['*.pl']}, 18 | include_package_data=True, 19 | install_requires=[ 20 | "torch", 21 | "torchvision", 22 | "numpy", 23 | "pandas", 24 | "pyparsing", 25 | "dgl", 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /tests/test_anbncn.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from examples.anbncn.anbncn_data import ( 4 | create_anbncn_language, 5 | create_non_anbncn_language, 6 | ) 7 | 8 | 9 | class BracketTest(unittest.TestCase): 10 | def test_anbncn_creation(self): 11 | self.assertEqual(["abc"], create_anbncn_language(min_length=3, max_length=3)) 12 | self.assertEqual( 13 | ["abc", "aabbcc"], create_anbncn_language(min_length=3, max_length=6) 14 | ) 15 | self.assertEqual( 16 | ["abc", "aabbcc", "aaabbbccc"], 17 | create_anbncn_language(min_length=3, max_length=9), 18 | ) 19 | 20 | def test_non_anbncn_creation_empty(self): 21 | self.assertEqual( 22 | [], 23 | create_non_anbncn_language( 24 | min_length=3, max_length=3, allow_non_threefold=False 25 | ), 26 | ) 27 | self.assertEqual( 28 | [], 29 | create_non_anbncn_language( 30 | min_length=3, max_length=5, allow_non_threefold=False 31 | ), 32 | ) 33 | 34 | def test_non_anbncn_creation(self): 35 | expected = list( 36 | [ 37 | "aaaabc", 38 | "aaabbc", 39 | "aaabcc", 40 | "aabbbc", 41 | "aabccc", 42 | "abbbbc", 43 | "abbbcc", 44 | "abbccc", 45 | "abcccc", 46 | ] 47 | ) 48 | expected.sort() 49 | calculated = create_non_anbncn_language( 50 | min_length=3, max_length=6, allow_non_threefold=False 51 | ) 52 | calculated.sort() 53 | self.assertEqual(expected, calculated) 54 | 55 | def test_non_anbncn_creation_larger(self): 56 | calculated = create_non_anbncn_language( 57 | min_length=3, max_length=12, allow_non_threefold=False 58 | ) 59 | print(calculated) 60 | self.assertLess(20, len(calculated)) 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /tests/test_bracket.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import examples.bracket.bracket 4 | from examples.bracket.bracket_data import ( 5 | create_bracket_language_of_length, 6 | create_bracket_language, 7 | BracketDataset, 8 | ) 9 | from deepstochlog.dataloader import DataLoader 10 | from deepstochlog.model import DeepStochLogModel 11 | # from utils import calculate_probability_predictions 12 | 13 | 14 | class BracketTest(unittest.TestCase): 15 | def test_bracket_creation_exact_length(self): 16 | self.assertEqual(["()"], create_bracket_language_of_length(2)) 17 | self.assertEqual({"()()", "(())"}, set(create_bracket_language_of_length(4))) 18 | self.assertEqual( 19 | {"(()())", "((()))", "()()()", "()(())", "(())()"}, 20 | set(create_bracket_language_of_length(6)), 21 | ) 22 | self.assertEqual( 23 | { 24 | "((()()))", 25 | "(((())))", 26 | "(()()())", 27 | "(()(()))", 28 | "((())())", 29 | "(()())()", 30 | "((()))()", 31 | "()()()()", 32 | "()(())()", 33 | "(())()()", 34 | "()(()())", 35 | "()((()))", 36 | "()()()()", 37 | "()()(())", 38 | "()(())()", 39 | "()()()()", 40 | "(())()()", 41 | "()()(())", 42 | "(())(())", 43 | }, 44 | set(create_bracket_language_of_length(8)), 45 | ) 46 | 47 | def test_bracket_creation_max_length(self): 48 | self.assertEqual(["()"], create_bracket_language(0, 2)) 49 | self.assertEqual({"()", "()()", "(())"}, set(create_bracket_language(0, 4))) 50 | self.assertEqual( 51 | len(set(create_bracket_language(0, 10))), 52 | len(create_bracket_language(0, 10)), 53 | ) 54 | 55 | def test_lower_than_one_probability(self): 56 | max_length = 10 57 | 58 | # Train model 59 | results = examples.bracket.bracket.run( 60 | min_length=2, 61 | max_length=max_length, 62 | allow_uneven=False, 63 | epochs=1, 64 | train_size=500, 65 | test_size=None, 66 | seed=42, 67 | ) 68 | model: DeepStochLogModel = results["model"] 69 | 70 | # Create test data 71 | test_size = 100 72 | test_data = BracketDataset( 73 | split="test", 74 | size=test_size, 75 | min_length=2, 76 | max_length=max_length, 77 | allow_uneven=False, 78 | seed=42, 79 | ) 80 | 81 | # Predict for test data 82 | # all_expected, all_predicted = calculate_probability_predictions( 83 | # model, DataLoader(test_data, batch_size=test_size) 84 | # ) 85 | 86 | # for predicted in all_predicted: 87 | # self.assertGreaterEqual(1, predicted) 88 | # self.assertLessEqual(0, predicted) 89 | 90 | 91 | if __name__ == "__main__": 92 | unittest.main() 93 | -------------------------------------------------------------------------------- /tests/test_citeseer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import examples.bracket.bracket 4 | from examples.bracket.bracket_data import ( 5 | create_bracket_language_of_length, 6 | create_bracket_language, 7 | BracketDataset, 8 | ) 9 | from deepstochlog.dataloader import DataLoader 10 | from deepstochlog.model import DeepStochLogModel 11 | 12 | # from utils import calculate_probability_predictions 13 | 14 | 15 | from pathlib import Path 16 | from typing import Union 17 | from shutil import copy2 18 | import torch 19 | import numpy as np 20 | from torch.optim import Adam 21 | from time import time 22 | 23 | from deepstochlog.network import Network, NetworkStore 24 | from examples.citeseer.with_rule_weights.citeseer_data_withrules import ( 25 | train_dataset, 26 | valid_dataset, 27 | test_dataset, 28 | queries_for_model, 29 | citations, 30 | pretraining_data, 31 | ) 32 | from examples.citeseer.citeseer_utils import ( 33 | create_model_accuracy_calculator, 34 | Classifier, 35 | RuleWeights, 36 | AccuracyCalculator, 37 | pretraining, 38 | ) 39 | from deepstochlog.utils import set_fixed_seed 40 | from deepstochlog.dataloader import DataLoader 41 | from deepstochlog.model import DeepStochLogModel 42 | from deepstochlog.trainer import DeepStochLogTrainer, print_logger, PrintFileLogger 43 | from deepstochlog.term import Term, List 44 | 45 | root_path = Path(__file__).parent 46 | 47 | root = Path(__file__).parent 48 | with open( 49 | root 50 | / ".." 51 | / "examples" 52 | / "citeseer" 53 | / "with_rule_weights" 54 | / "citeseer_ruleweights.pl", 55 | "r", 56 | ) as file: 57 | citeseer_program = file.read() 58 | 59 | 60 | class CiteseerTest(unittest.TestCase): 61 | def test_citeseer_and_or_tree(self): 62 | 63 | # Load the MNIST model, and Adam optimiser 64 | input_size = len(train_dataset.documents[0]) 65 | classifier = Classifier(input_size=input_size) 66 | rule_weights = RuleWeights(num_rules=2, num_classes=6) 67 | classifier_network = Network( 68 | "classifier", 69 | classifier, 70 | index_list=[Term("class" + str(i)) for i in range(6)], 71 | ) 72 | rule_weight = Network( 73 | "rule_weight", 74 | rule_weights, 75 | index_list=[Term(str("neural")), Term(str("cite"))], 76 | ) 77 | networks = NetworkStore(classifier_network, rule_weight) 78 | 79 | citations = """ 80 | cite(0,1). 81 | cite(1,3).""" 82 | 83 | queries_for_model = [Term("s", Term("_"), List("0"))] 84 | # ,Term("s", Term("class4"), List("1"))] 85 | 86 | model = DeepStochLogModel.from_string( 87 | program_str=citeseer_program, 88 | query=queries_for_model, 89 | networks=networks, 90 | prolog_facts=citations, 91 | normalization=DeepStochLogModel.FULL_NORM, 92 | ) 93 | 94 | tree = model.trees 95 | 96 | print(tree) 97 | 98 | 99 | if __name__ == "__main__": 100 | unittest.main() 101 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from examples.bracket import bracket 4 | from examples.addition import addition 5 | from examples.mathexpression import mathexpression 6 | from examples.wap import wap 7 | 8 | 9 | class DeepStochLogTest(unittest.TestCase): 10 | def test_no_exception_addition(self): 11 | result = addition.run(epochs=1, train_size=1, test_size=1, verbose=False) 12 | self.assertTrue(result["proving_time"] > 0) 13 | 14 | def test_no_exception_brackets(self): 15 | result = bracket.run( 16 | max_length=4, 17 | train_size=1, 18 | epochs=1, 19 | test_size=5, 20 | test_example_idx=[0, 1], 21 | verbose=True, 22 | allow_uneven=False, 23 | ) 24 | self.assertTrue(result["grounding_time"] > 0) 25 | 26 | def test_no_exception_expression(self): 27 | time = mathexpression.run( 28 | epochs=1, 29 | train_size=1, 30 | test_size=1, 31 | verbose=False, 32 | expression_max_length=3, 33 | device_str="cpu", 34 | ) 35 | self.assertTrue(time > 0) 36 | 37 | def test_no_exception_wap(self): 38 | result = wap.run( 39 | epochs=1, 40 | train_size=1, 41 | test_size=1, 42 | verbose=False, 43 | ) 44 | self.assertTrue(result["grounding_time"] > 0) 45 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import List 3 | 4 | from pyparsing import ParseException 5 | 6 | from deepstochlog.parser import parse_rules 7 | from deepstochlog.rule import ( 8 | NDCGRule, 9 | NeuralProbabilityAnnotation, 10 | StaticProbabilityAnnotation, 11 | TrainableProbabilityAnnotation, 12 | Rule, 13 | Fact, 14 | ClauseRule, 15 | ) 16 | 17 | 18 | class ParserTestCase(unittest.TestCase): 19 | def test_static_probability(self): 20 | test_program = "0.2 :: foo --> a." 21 | result = parse_rules(test_program).rules 22 | self.assertEqual("0.2", str(result[0].probability)) 23 | 24 | def test_body_period_exceptions_parsing(self): 25 | body = 'a(0.4), 0.3, ["hello."], {b(X,Y)}' 26 | test_program = "0.2 :: foo --> " + body + ".\n" "0.1 :: bar --> a." 27 | resulting_rules: List[NDCGRule] = parse_rules(test_program).get_ndcg_rules() 28 | 29 | print("Resulting rules:\n", resulting_rules) 30 | 31 | self.assertEqual(2, len(resulting_rules)) 32 | self.assertEqual(body, resulting_rules[0].body) 33 | 34 | program_str = "\n".join([str(r) for r in resulting_rules]) 35 | print(program_str) 36 | self.assertEqual( 37 | test_program.replace(" ", ""), 38 | program_str.replace(" ", ""), 39 | ) 40 | 41 | def test_all_types_probability(self): 42 | test_program = ( 43 | "0.2 :: foo --> a.\n" 44 | "nn(mnist, [X], Y, digit) :: bla(A,B) --> bla, h.\n" 45 | "t(_) :: bar(X) --> a(0,1), c(_,X)." 46 | ) 47 | program = parse_rules(test_program) 48 | resulting_rules: List[NDCGRule] = program.get_ndcg_rules() 49 | self.assertEqual(3, len(resulting_rules)) 50 | self.assertEqual( 51 | StaticProbabilityAnnotation, type(resulting_rules[0].probability) 52 | ) 53 | self.assertEqual( 54 | NeuralProbabilityAnnotation, type(resulting_rules[1].probability) 55 | ) 56 | self.assertEqual( 57 | TrainableProbabilityAnnotation, type(resulting_rules[2].probability) 58 | ) 59 | 60 | program_str = "\n".join([str(r) for r in resulting_rules]) 61 | print(program_str) 62 | self.assertEqual( 63 | test_program.replace(" ", ""), 64 | program_str.replace(" ", ""), 65 | ) 66 | 67 | def test_fact(self): 68 | fact_rule = parse_rules("a.").rules 69 | print(fact_rule) 70 | self.assertEqual(1, len(fact_rule)) 71 | self.assertEqual("a.", str(fact_rule[0])) 72 | self.assertTrue(type(fact_rule[0]) == Fact) 73 | 74 | def test_all_types_rules(self): 75 | test_program = """ 76 | a. 77 | b :- c. 78 | 1.0 :: e --> f. 79 | """ 80 | resulting_rules: List[Rule] = parse_rules(test_program).rules 81 | self.assertEqual(3, len(resulting_rules)) 82 | self.assertEqual(Fact, type(resulting_rules[0])) 83 | self.assertEqual(ClauseRule, type(resulting_rules[1])) 84 | self.assertEqual(NDCGRule, type(resulting_rules[2])) 85 | 86 | program_str = "\n".join([str(r) for r in resulting_rules]) 87 | print(program_str) 88 | self.assertEqual( 89 | test_program.replace(" ", "").strip(), 90 | program_str.replace(" ", "").strip(), 91 | ) 92 | 93 | def test_term_arguments(self): 94 | program_str = """ 95 | 1.0 :: rep(s(N), C) --> [X]. 96 | """ 97 | program = parse_rules(program_str) 98 | self.assertEqual(1, len(program.rules)) 99 | self.assertTrue(isinstance(program.rules[0], NDCGRule)) 100 | 101 | program_stringified = str(program) 102 | self.assertEqual( 103 | program_str.replace(" ", "").strip(), 104 | program_stringified.replace(" ", "").strip(), 105 | ) 106 | 107 | def test_forgotten_period_exception_raised(self): 108 | program_str = """ 109 | letter(X) :- member(X,[a,b,c]) 110 | nn(mnist, [X], C, letter) :: rep(s(N), C) --> [X], rep(N, C). 111 | """ 112 | self.assertRaises(ParseException, parse_rules, program_str) 113 | 114 | def test_anbncn(self): 115 | program_str = """ 116 | 0.5 :: s(0) --> akblcm(K,L,M),{K\\=L; L\\=M; M\\=K}, {K \\= 0, L \\= 0, M \\= 0}. 117 | 0.5 :: s(1) --> akblcm(N,N,N). 118 | akblcm(K,L,M) --> rep(K,A), rep(L,B), rep(M,C),{A\\=B, B\\=C, C\\=A}. 119 | rep(0, _) --> []. 120 | rep(s(N), C) --> [X], rep(N,C), {domain(C, [a,b,c]), nn(mnist, X, C)}. 121 | """ 122 | program = parse_rules(program_str) 123 | self.assertEqual(5, len(program.rules)) 124 | 125 | def test_trainable_probability(self): 126 | program_str = """ 127 | t(_) :: a --> b. 128 | t(_) :: a --> c. 129 | t(_) :: a --> d. 130 | """ 131 | program = parse_rules(program_str) 132 | 133 | # Basic sanity checks in parser 134 | self.assertEqual(3, len(program.rules)) 135 | for i in range(3): 136 | rule: Rule = program.rules[i] 137 | self.assertTrue(isinstance(rule, NDCGRule)) 138 | if isinstance(rule, NDCGRule): 139 | self.assertTrue( 140 | isinstance(rule.probability, TrainableProbabilityAnnotation) 141 | ) 142 | else: 143 | self.fail("Not a proper NDCG rule: " + str(rule)) 144 | self.assertTrue(program.has_trainable_probabilities()) 145 | 146 | # Transform program 147 | program, networks = program._transform_trainable_probabilities_to_switches() 148 | self.assertFalse(program.has_trainable_probabilities()) 149 | self.assertEqual(1, len(networks.networks)) 150 | print(program) 151 | 152 | 153 | if __name__ == "__main__": 154 | unittest.main() 155 | --------------------------------------------------------------------------------