Skip to content

Commit

Permalink
Updated NNET Package (#23)
Browse files Browse the repository at this point in the history
major updates
  • Loading branch information
Aero-Spec authored Dec 15, 2024
1 parent 4411dd4 commit cd89c2e
Show file tree
Hide file tree
Showing 28 changed files with 1,144 additions and 1,083 deletions.
7 changes: 7 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
[run]
branch = True
source = .

[report]
exclude_lines =
if __name__ == '__main__':

[html]
directory = coverage_html_report
70 changes: 70 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: CI Build and Test

on:
push:
branches:
- main # Run on push to the main branch
pull_request:
branches:
- main # Run on pull requests to the main branch
workflow_dispatch:

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
# Step 1: Check out the repository
- name: Check out the repository
uses: actions/checkout@v4

# Step 2: Set up Python environment
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Install Python Dependencies
run: |
python -m pip install --upgrade pip
pip install -r test_requirements.txt
- name: Run Python Tests with Coverage
run: |
pytest --maxfail=5 --disable-warnings --cov=. --cov-report=xml
- name: Upload Python Coverage to Codecov
uses: codecov/codecov-action@v4
with:
flags: python
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

# Step 3: Install C++ build tools
- name: Install C++ Build Tools
run: sudo apt-get update && sudo apt-get install -y build-essential cmake g++ lcov

# Step 4: Configure and Build C++ Project
- name: Configure and Build C++ Project
run: |
mkdir -p build
cd build
cmake -DCMAKE_CXX_FLAGS="--coverage" .. # Enable coverage flags
make
# Step 5: Run C++ Tests
- name: Run C++ Tests
run: |
cd build
./NNet # Replace `NNet` with your executable name
# Step 6: Generate C++ Coverage Report
- name: Generate C++ Coverage Report
run: |
cd build
lcov --capture --directory . --output-file coverage.info
lcov --list coverage.info
# Step 7: Upload C++ Coverage to Codecov
- name: Upload C++ Coverage to Codecov
uses: codecov/codecov-action@v4
with:
flags: cpp
files: build/coverage.info # Path to the coverage report
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
11 changes: 0 additions & 11 deletions .travis.yml

This file was deleted.

24 changes: 24 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
cmake_minimum_required(VERSION 3.10)

# Project name
project(NNet)

# Set C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# Add source directory
include_directories(cpp)

# Add the executable
add_executable(NNet
cpp/main.cpp
cpp/nnet.cpp
)

# Enable code coverage flags for GCC/Clang
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
message(STATUS "Enabling code coverage flags")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -O0 --coverage")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
endif()
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
## NNet Repository

[![Build Status](https://travis-ci.org/sisl/NNet.svg?branch=master)](https://travis-ci.org/sisl/NNet)
[![Coverage Status](https://coveralls.io/repos/github/sisl/NNet/badge.svg?branch=master&service=github)](https://coveralls.io/github/sisl/NNet?branch=master)
[![CI Build](https://github.com/sisl/NNet/actions/workflows/CI.yml/badge.svg)](https://github.com/sisl/NNet/actions/workflows/CI.yml)
[![codecov](https://codecov.io/gh/sisl/NNet/graph/badge.svg?token=6ACfFDS9CG)](https://codecov.io/gh/sisl/NNet)

### Introduction
The .nnet file format for fully connected ReLU networks was originially created in 2016 to define aircraft collision avoidance neural networks in a human-readable text document. Since then it was incorporated into the Reluplex repository and used to define benchmark neural networks. This format is a simple text-based format for feed-forward, fully-connected, ReLU-activated neural networks. It is not affiliated with Neuroph or other frameworks that produce files with the .nnet extension.

This repository contains documentation for the .nnet format as well as useful functions for working with the networks. The nnet folder contains example neural network files. The converters folder contains functions to convert the .nnet files to Tensorflow, ONNX, and Keras formats and vice-versa. The python, julia, and cpp folders contain python, julia, and C++ functions for reading and evaluating .nnet networks. The examples folder provides python examples for using the available functions.

This repository is set up as a python package. To run the examples, make sure that the folder in which this repository resides (the parent directory of NNet) is added to the PYTHONPATH environment variable.
---

### File format of .nnet
The file begins with header lines, some information about the network architecture, normalization information, and then model parameters. Line by line:<br/><br/>
Expand All @@ -26,11 +27,20 @@ The minimum/maximum input values are used to define the range of input values se

The mean/range values are the values used to normalize the network training data before training the network. The normalization substracts the mean and divides by the range, giving a distribution that is zero mean and unit range. Therefore, new inputs to the network should be normalized as well, so there is a mean/range value for every input to the network. There is also an additional mean/range value for the network outputs, but just one value for all outputs. The raw network outputs can be re-scaled by multiplying by the range and adding the mean.

---

### Writing .nnet files
In the utils folder, the file writeNNet.py contains a python method for writing neural network data to a .nnet file. The main method, writeNNet, requires a list of weights, biases, minimum input values, maximum input values, mean of inputs/ouput, and range of inputs/output, and a filename to write the neural network.


---

### Loading and evaluating .nnet files
There are three folders for C++, Julia, and Python examples. Each subfolder contains a nnet.* file that contains functions for loading the network from a .nnet file and then evaluating a set of inputs given the loaded model. There are examples in each folder to demonstrate how the functions can be used.

---

## License
This code is licensed under the MIT license. See LICENSE for details.

---
150 changes: 83 additions & 67 deletions converters/nnet2onnx.py
Original file line number Diff line number Diff line change
@@ -1,86 +1,102 @@
import numpy as np
import sys
import onnx
import numpy as np
from onnx import helper, numpy_helper, TensorProto
from NNet.utils.readNNet import readNNet
from NNet.utils.normalizeNNet import normalizeNNet
import argparse
from typing import Optional

def nnet2onnx(
nnetFile: str,
onnxFile: Optional[str] = "",
outputVar: str = "y_out",
inputVar: str = "X",
normalizeNetwork: bool = False
) -> None:
"""
Convert a .nnet file to ONNX format.
def nnet2onnx(nnetFile, onnxFile="", outputVar = "y_out", inputVar="X", normalizeNetwork=False):
'''
Convert a .nnet file to onnx format
Args:
nnetFile: (string) .nnet file to convert to onnx
onnxFile: (string) Optional, name for the created .onnx file
outputName: (string) Optional, name of the output variable in onnx
normalizeNetwork: (bool) If true, adapt the network weights and biases so that
networks and inputs do not need to be normalized. Default is False.
'''
if normalizeNetwork:
weights, biases = normalizeNNet(nnetFile)
else:
weights, biases = readNNet(nnetFile)

nnetFile (str): Path to the .nnet file to convert.
onnxFile (Optional[str]): Optional, name for the created .onnx file. Defaults to the same name as the .nnet file.
outputVar (str): Optional, name of the output variable in ONNX. Defaults to 'y_out'.
inputVar (str): Name of the input variable in ONNX. Defaults to 'X'.
normalizeNetwork (bool): If True, adapt the network weights and biases so that networks and inputs
do not need normalization. Defaults to False.
"""
try:
if normalizeNetwork:
weights, biases = normalizeNNet(nnetFile)
else:
weights, biases = readNNet(nnetFile)
except FileNotFoundError:
print(f"Error: The file {nnetFile} was not found.")
sys.exit(1)

inputSize = weights[0].shape[1]
outputSize = weights[-1].shape[0]
numLayers = len(weights)
# Default onnx filename if none specified
if onnxFile=="":
onnxFile = nnetFile[:-4]+'onnx'
# Initialize graph
inputs = [helper.make_tensor_value_info(inputVar, TensorProto.FLOAT, [inputSize])]

# Default ONNX filename if none specified
if not onnxFile:
onnxFile = f"{nnetFile[:-5]}.onnx"

# Initialize the graph
inputs = [helper.make_tensor_value_info(inputVar, TensorProto.FLOAT, [inputSize])]
outputs = [helper.make_tensor_value_info(outputVar, TensorProto.FLOAT, [outputSize])]
operations = []
initializers = []
# Loop through each layer of the network and add operations and initializers

# Build the ONNX model layer by layer
for i in range(numLayers):

# Use outputVar for the last layer
outputName = "H%d"%i
if i==numLayers-1:
# Use the output variable name for the last layer
outputName = f"H{i}"
if i == numLayers - 1:
outputName = outputVar

# Weight matrix multiplication
operations.append(helper.make_node("MatMul",["W%d"%i,inputVar],["M%d"%i]))
initializers.append(numpy_helper.from_array(weights[i].astype(np.float32),name="W%d"%i))
# Bias add
operations.append(helper.make_node("Add",["M%d"%i,"B%d"%i],[outputName]))
initializers.append(numpy_helper.from_array(biases[i].astype(np.float32),name="B%d"%i))
# Use Relu activation for all layers except the last layer
if i<numLayers-1:
operations.append(helper.make_node("Relu",["H%d"%i],["R%d"%i]))
inputVar = "R%d"%i
# Create the graph and model in onnx
graph_proto = helper.make_graph(operations,"nnet2onnx_Model",inputs, outputs,initializers)
operations.append(helper.make_node("MatMul", [f"W{i}", inputVar], [f"M{i}"]))
initializers.append(numpy_helper.from_array(weights[i].astype(np.float32), name=f"W{i}"))

# Bias addition
operations.append(helper.make_node("Add", [f"M{i}", f"B{i}"], [outputName]))
initializers.append(numpy_helper.from_array(biases[i].astype(np.float32), name=f"B{i}"))

# Apply ReLU activation to all layers except the last
if i < numLayers - 1:
operations.append(helper.make_node("Relu", [f"H{i}"], [f"R{i}"]))
inputVar = f"R{i}"

# Create the graph and model in ONNX format
graph_proto = helper.make_graph(operations, "nnet2onnx_Model", inputs, outputs, initializers)
model_def = helper.make_model(graph_proto)

# Print statements
print("Converted NNet model at %s"%nnetFile)
print(" to an ONNX model at %s"%onnxFile)

# Additional print statements if desired
#print("\nReadable GraphProto:\n")
#print(helper.printable_graph(graph_proto))

# Save the ONNX model
# Print success message
print(f"Converted NNet model at {nnetFile} to an ONNX model at {onnxFile}")

# Save the ONNX model to file
onnx.save(model_def, onnxFile)


if __name__ == '__main__':
# Read user inputs and run nnet2onnx function for different numbers of inputs
if len(sys.argv)>1:
nnetFile = sys.argv[1]
if len(sys.argv)>2:
onnxFile = sys.argv[2]
if len(sys.argv)>3:
outputName = argv[3]
nnet2onnx(nnetFile,onnxFile,outputName)
else: nnet2onnx(nnetFile,onnxFile)
else: nnet2onnx(nnetFile)
else:
print("Need to specify which .nnet file to convert to ONNX!")

def main():
# Parse command-line arguments
parser = argparse.ArgumentParser(description="Convert a .nnet file to ONNX format.")
parser.add_argument("nnetFile", type=str, help="The .nnet file to convert")
parser.add_argument("--onnxFile", type=str, default="", help="Optional: Name of the output ONNX file")
parser.add_argument("--outputVar", type=str, default="y_out", help="Optional: Name of the output variable")
parser.add_argument("--inputVar", type=str, default="X", help="Optional: Name of the input variable")
parser.add_argument("--normalize", action="store_true", help="Normalize network weights and biases")

args = parser.parse_args()

# Call the nnet2onnx function with parsed arguments
nnet2onnx(
nnetFile=args.nnetFile,
onnxFile=args.onnxFile,
outputVar=args.outputVar,
inputVar=args.inputVar,
normalizeNetwork=args.normalize
)

if __name__ == "__main__":
main()
Loading

0 comments on commit cd89c2e

Please sign in to comment.