Skip to content

Commit

Permalink
adding in sympy formula generation
Browse files Browse the repository at this point in the history
  • Loading branch information
KristofPusztai committed May 20, 2024
1 parent 5469584 commit e901ec1
Show file tree
Hide file tree
Showing 7 changed files with 352 additions and 245 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tensorflow==2.12.0 pytest pytest-cov
pip install tensorflow==2.12.0 pytest-cov sympy
- name: Run Pytests
run: pytest --cov=EQL --cov-report=xml
- name: Upload coverage reports to Codecov
Expand Down
20 changes: 17 additions & 3 deletions EQL/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ def sigmoid(out, index):
def mult(out, index):
sum1 = tf.gather(out, [index], axis=1)
sum2 = tf.gather(out, [index + 1], axis=1)
sum_input = tf.add(sum1, sum2)
return tf.multiply(sum_input, sum_input, name='mult_output')
return tf.multiply(sum1, sum2, name='mult_output')


class EqlLayer(keras.layers.Layer):
Expand Down Expand Up @@ -55,6 +54,13 @@ def __init__(self, w_initializer, b_initializer, v, lmbda=0, mask=None, exclude=
self.exclusion += 2
self.activations.remove(mult)

def _mask(self):
for i in range(self.w.shape[0]):
w_mask = tf.matmul([self.w[i]], self.mask[0][i])[0]
self.w[i].assign(w_mask)
b_mask = tf.matmul([self.b], self.mask[1])[0]
self.b.assign(b_mask)

def build(self, input_shape):
self.w = self.add_weight(
shape=(input_shape[-1], 6 * self.v - self.v * self.exclusion),
Expand Down Expand Up @@ -93,9 +99,17 @@ def __init__(self, w_initializer, b_initializer, lmbda=0, mask=None):
self.b_initializer = initializers.get(b_initializer)
self.mask = mask


def _mask(self):
for i in range(self.w.shape[0]):
w_mask = tf.matmul([self.w[i]], self.mask[0][i])[0]
self.w[i].assign(w_mask)
b_mask = tf.matmul([self.b], self.mask[1])[0]
self.b.assign(b_mask)

def build(self, input_shape):
self.w = self.add_weight(
shape=(input_shape[-1], 1),
shape=(input_shape[-1], 1), #TODO: Output of dense layer is 1, maybe change this for multi-dimensionality
initializer=self.w_initializer,
trainable=True, regularizer=self.regularizer
)
Expand Down
73 changes: 73 additions & 0 deletions EQL/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from tensorflow import keras
import numpy as np
from sympy import *
from math import floor
from EQL.layer import EqlLayer, DenseLayer


Expand Down Expand Up @@ -313,6 +315,8 @@ def fit(self, x, y, lmbda, t0=100, t1=0, t2=0, initial_epoch=0, verbose=0, batch
sample_weight, initial_epoch, steps_per_epoch,
validation_steps, validation_batch_size, validation_freq,
max_queue_size, workers, use_multiprocessing)
for layer in self.model.layers[1:]:
layer._mask()

def predict(self, x, batch_size=None, verbose=0, steps=None, callbacks=None, max_queue_size=10,
workers=1, use_multiprocessing=False):
Expand Down Expand Up @@ -381,6 +385,75 @@ def get_weights(self, layer):
"""
return self.model.layers[layer].get_weights()

def formula(self, raw_latex=False, reduce=True):
init_printing()

num_layers = self.num_layers + 1 # EQL + Dense
input_dim = len(self.get_weights(1)[0])

output = {}
# Looping through EQL layers
for layer in range(1, num_layers):
previous_output = output
output = {}
input_dim = len(self.get_weights(layer)[0])
prev_keys = list(previous_output.keys())
# Looping through input dimension of layer
for dim in range(input_dim):
layer_weights = self.get_weights(layer)[0][dim]
layer_biases = self.get_weights(layer)[1]
weight_index = 0
# Handling first layer defining sympy symbols
if layer == 1:
symbol = symbols(f'x_{dim+1}')
else:
symbol = previous_output[prev_keys[dim]]
# Looping through number of nodes in next layer, v * activations
for num_repetition in range(self.v[layer-1]):
for activation in self.model.layers[layer].activations:
if activation.__name__ == 'mult':
if dim == 0:
output[f'{activation.__name__}{num_repetition}'] = [layer_biases[weight_index], layer_biases[weight_index+1]]
output[f'{activation.__name__}{num_repetition}'][0] += layer_weights[weight_index] * symbol
output[f'{activation.__name__}{num_repetition}'][1] += layer_weights[weight_index+1] * symbol
weight_index += 1 # Skip over second mult node
else:
if dim == 0:
output[f'{activation.__name__}{num_repetition}'] = layer_biases[weight_index]
output[f'{activation.__name__}{num_repetition}'] += layer_weights[weight_index] * symbol
weight_index += 1

for activation in output.keys():
if 'sin' in activation:
output[activation] = sin(output[activation])
elif 'cos' in activation:
output[activation] = cos(output[activation])
elif 'identity' in activation:
continue
elif 'sigmoid' in activation:
output[activation] = Function('\sigma')(output[activation])
elif 'mult' in activation:
output[activation] = output[activation][0] * output[activation][1]
else:
raise Exception(activation, 'Not a valid activation')

# DenseLayer
f = 0
layer = self.get_weights(len(self.model.layers)-1)
# Looping through weights for each input dimension
for dim in range(len(layer[0])):
if dim == 0:
f += layer[1][0]
# Looping through activation functions of previous layer
f += layer[0][dim][0] * output[list(output.keys())[dim]]

if reduce:
f = simplify(f)
if raw_latex:
return latex(f)
else:
return f

def set_weights(self, layer, weights):
"""
:param layer: Specified layer number to set weights in
Expand Down
472 changes: 239 additions & 233 deletions Jupyter Notebooks/EQLtest.ipynb

Large diffs are not rendered by default.

6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
[![GitHub Action Badge](https://github.com/KristofPusztai/EQL/actions/workflows/pytest.yml/badge.svg)](https://github.com/KristofPusztai/EQL/actions)

[![codecov](https://codecov.io/gh/KristofPusztai/EQL/graph/badge.svg?token=5BLB6GHC7S)](https://codecov.io/gh/KristofPusztai/EQL)

[![GitHub release (latest by date)](https://img.shields.io/github/v/release/KristofPusztai/EQL?style=plastic)](https://pypi.org/project/EQL-NN/)

[![PyPI - License](https://img.shields.io/pypi/l/EQL-NN)](https://opensource.org/license/mit/)

[![Downloads](https://static.pepy.tech/badge/eql-nn)](https://pepy.tech/project/eql-nn)

[![GitHub issues](https://img.shields.io/github/issues/KristofPusztai/EQL)](https://github.com/KristofPusztai/EQL/issues)

# Introduction:
Expand Down Expand Up @@ -61,6 +56,7 @@ in your model.
EQLmodel.count_params() # Provides # trainable params
EQLmodel.get_weights(layer) #returns array of layer values
EQLmodel.set_weights(layer, weights) #sets weights of specified layer
EQLmodel.formula(raw_latex=False, reduce=True) # Returns interpretable equations via sympy

EQLmodel.evaluate(x=None, y=None, batch_size=None, verbose=1,
sample_weight=None, steps=None,
Expand Down
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="EQL-NN",
version="2.0.2", # MAJOR.MINOR.MAINTENANCE
version="2.1.0", # MAJOR.MINOR.MAINTENANCE
author="Kristof Pusztai",
author_email="kpusztai@berkeley.edu",
description="A Tensorflow implementation of the Equation Learning Based Neural Network Model",
Expand All @@ -15,7 +15,8 @@
packages=setuptools.find_packages(),
install_requires=[
'tensorflow>=2.11',
'numpy'
'numpy',
'sympy'
],
classifiers=[
"Programming Language :: Python :: 3",
Expand Down
19 changes: 18 additions & 1 deletion tests/overall_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import tensorflow as tf
import numpy as np

import sys

sys.path[0] = sys.path[0][:-6] # Adding parent directory to path for EQL imports below

from EQL.layer import EqlLayer, DenseLayer
Expand Down Expand Up @@ -147,3 +147,20 @@ def test10(): # Tests layer exclusion parameter
test.fit(x, y, 0.0, t0=1)
params = test.count_params()
assert params == 10, 'trainable parameter count is wrong'

def test11(): # Tests simple sympy formula creation
EQLmodel = EQL(num_layers = 1)
EQLmodel.build_and_compile_model()
#Generating proper weights for EQL Layer
w1 = EQLmodel.model.layers[1].get_weights()
w1[0] = np.array([[0, 1, 0, 0, 0, 0]])
w1[1] = np.array([0,0,0,0,0,0])

#Generating proper weights for Dense Layer
w2 = EQLmodel.model.layers[2].get_weights()
w2[0] = np.array([[0],[1],[0],[0],[0]])
w2[1] = np.array([0])

EQLmodel.set_weights(1, w1)
EQLmodel.set_weights(2, w2)
assert EQLmodel.formula(raw_latex = True) == '1.0 \\sin{\\left(1.0 x_{1} \\right)}', 'latex output is wrong'

0 comments on commit e901ec1

Please sign in to comment.