Skip to content

Commit

Permalink
neuralang first doc
Browse files Browse the repository at this point in the history
  • Loading branch information
maniospas committed May 28, 2024
1 parent a135e17 commit 4fbd138
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 44 deletions.
1 change: 0 additions & 1 deletion JGNN/src/examples/nodeClassification/GCN.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import mklab.JGNN.adhoc.ModelBuilder;
import mklab.JGNN.adhoc.datasets.Cora;
import mklab.JGNN.adhoc.parsers.FastBuilder;
import mklab.JGNN.adhoc.parsers.TextBuilder;
import mklab.JGNN.core.Matrix;
import mklab.JGNN.nn.Model;
import mklab.JGNN.nn.ModelTraining;
Expand Down
21 changes: 7 additions & 14 deletions JGNN/src/examples/nodeClassification/Scripting.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package nodeClassification;

import java.nio.file.Files;
import java.nio.file.Paths;

import mklab.JGNN.adhoc.Dataset;
import mklab.JGNN.adhoc.ModelBuilder;
import mklab.JGNN.adhoc.datasets.Cora;
import mklab.JGNN.adhoc.parsers.TextBuilder;
import mklab.JGNN.adhoc.parsers.Neuralang;
import mklab.JGNN.core.Matrix;
import mklab.JGNN.nn.Model;
import mklab.JGNN.nn.ModelTraining;
Expand All @@ -15,7 +14,6 @@
import mklab.JGNN.core.empy.EmptyTensor;
import mklab.JGNN.nn.initializers.XavierNormal;
import mklab.JGNN.nn.loss.CategoricalCrossEntropy;
import mklab.JGNN.nn.optimizers.Adam;

/**
* Demonstrates classification with an architecture defined through the scripting engine.
Expand All @@ -26,24 +24,19 @@ public class Scripting {
public static void main(String[] args) throws Exception {
Dataset dataset = new Cora();
dataset.graph().setMainDiagonal(1).setToSymmetricNormalization();
long numClasses = dataset.labels().getCols();

ModelBuilder modelBuilder = new TextBuilder()
.parse(String.join("\n", Files.readAllLines(Paths.get("../architectures.nn"))))
ModelBuilder modelBuilder = new Neuralang()
.parse(Paths.get("../architectures.nn"))
.constant("A", dataset.graph())
.constant("h", dataset.features())
.var("nodes")
.config("classes", numClasses)
.config("hidden", numClasses)
.out("classify(nodes, gcn(A,h))");
System.out.println(modelBuilder.getExecutionGraphDot());
modelBuilder
.config("classes", dataset.labels().getCols())
.config("hidden", 16)
.out("classify(nodes, gcn(A,h))")
.autosize(new EmptyTensor(dataset.samples().getSlice().size()));

ModelTraining trainer = new ModelTraining()
.setOptimizer(new Adam(modelBuilder.getConfigOrDefault("lr", 0.01)))
.setEpochs(modelBuilder.getConfigOrDefault("epochs", 1000))
.setPatience(modelBuilder.getConfigOrDefault("patience", 100))
.configFrom(modelBuilder)
.setVerbose(true)
.setLoss(new CategoricalCrossEntropy())
.setValidationLoss(new CategoricalCrossEntropy());
Expand Down
39 changes: 23 additions & 16 deletions JGNN/src/main/java/mklab/JGNN/adhoc/ModelBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ public ModelBuilder param(String name, double regularization, Tensor value) {
* @see #param(String, double, Tensor)
*/
public ModelBuilder config(String name, double value) {
configurations.put(name, value);
this.configurations.put(name, value);
return this;
}

Expand Down Expand Up @@ -908,7 +908,7 @@ else if(functions.containsKey(splt[2])) {
this.configurations = new HashMap<String, Double>(this.configurations);
HashMap<String, String> customNames = new HashMap<String, String>();
for(int i=0;i<args.length;i++)
if(i<splt.length-3)
if(!args[i].contains(":"))
customNames.put(args[i].trim(), splt[i+3]);
else {
String config = args[i].substring(0, args[i].indexOf(":")).trim();
Expand All @@ -917,20 +917,20 @@ else if(functions.containsKey(splt[2])) {
if(!this.configurations.containsKey(config))
throw new RuntimeException("Required external config: "+config);
}
else if(!this.configurations.containsKey(config))
this.config(config, parseConfigValue(value));
}
for(int i=args.length;i<splt.length-3;i++) {
String config = splt[i+3].substring(0, splt[i+3].indexOf(":")).trim();
String value = splt[i+3].substring(splt[i+3].indexOf(":")+1).trim();
if(value.equals("extern")) {
if(!this.configurations.containsKey(config))
throw new RuntimeException("Required external config: "+config);
this.config(config, parseConfigValue(value));
/*// these are parsed in the attempt to create an intermediate variable for the argument
if(i<splt.length-3) {
String config = splt[i+3].substring(0, splt[i+3].indexOf(":")).trim();
String value = splt[i+3].substring(splt[i+3].indexOf(":")+1).trim();
if(value.equals("extern")) {
if(!this.configurations.containsKey(config))
throw new RuntimeException("Required external config: "+config);
}
else
this.config(config, parseConfigValue(value));
}*/
}
else
this.config(config, parseConfigValue(value));
}

List<String> tokens = extractTokens(functions.get(splt[2]));
HashSet<String> keywords = new HashSet<String>();
keywords.addAll(functions.keySet());
Expand Down Expand Up @@ -970,16 +970,23 @@ else if(!keywords.contains(token) && !isNumeric(token) && !prevHash)
if(!prevHash && !prevTemp)
newExpr += token;
}
customNames.putAll(renameLater);
this.operation(newExpr);
this.configurations = configStack;
return this;
}
else
throw new RuntimeException("Invalid operation: "+desc);

if(arg0.contains(":"))
if(arg0.contains(":")) {
String config = arg0.substring(0, arg0.indexOf(":")).trim();
String value = arg0.substring(arg0.indexOf(":")+1).trim();
if(value.equals("extern")) {
if(!this.configurations.containsKey(config))
throw new RuntimeException("Required external config: "+config);
}
this.config(config, parseConfigValue(value));
return this;
}

if(arg0!=null) {
assertExists(arg0);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
package mklab.JGNN.adhoc.parsers;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import mklab.JGNN.adhoc.ModelBuilder;
import mklab.JGNN.core.Tensor;

public class TextBuilder extends ModelBuilder {
public TextBuilder() {
public class Neuralang extends ModelBuilder {
public Neuralang() {
}
public TextBuilder config(String name, double value) {
public Neuralang config(String name, double value) {
super.config(name, value);
return this;
}
public TextBuilder parse(String text) {
public Neuralang parse(Path path) {
try {
parse(String.join("\n", Files.readAllLines(path)));
} catch (IOException e) {
e.printStackTrace();
}
return this;
}

public Neuralang parse(String text) {
int depth = 0;
String progress = "";
for(int i=0;i<text.length();i++) {
Expand Down Expand Up @@ -42,15 +56,15 @@ else if(!progress.isEmpty())
operation(progress);
return this;
}
public TextBuilder constant(String name, Tensor value) {
public Neuralang constant(String name, Tensor value) {
super.constant(name, value);
return this;
}
public TextBuilder constant(String name, double value) {
public Neuralang constant(String name, double value) {
super.constant(name, value);
return this;
}
public TextBuilder var(String var) {
public Neuralang var(String var) {
super.var(var);
return this;
}
Expand Down
9 changes: 9 additions & 0 deletions JGNN/src/main/java/mklab/JGNN/nn/ModelTraining.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
import java.util.HashMap;
import java.util.List;

import mklab.JGNN.adhoc.ModelBuilder;
import mklab.JGNN.core.Matrix;
import mklab.JGNN.core.Memory;
import mklab.JGNN.core.Slice;
import mklab.JGNN.core.Tensor;
import mklab.JGNN.core.ThreadPool;
import mklab.JGNN.core.matrix.WrapRows;
import mklab.JGNN.nn.inputs.Parameter;
import mklab.JGNN.nn.optimizers.Adam;
import mklab.JGNN.nn.optimizers.BatchOptimizer;

/**
Expand Down Expand Up @@ -152,4 +154,11 @@ public void run() {
parameter.set(minLossParameters.get(parameter));
return model;
}
public ModelTraining configFrom(ModelBuilder modelBuilder) {
setOptimizer(new Adam(modelBuilder.getConfigOrDefault("lr", 0.01)));
setEpochs(modelBuilder.getConfigOrDefault("epochs", epochs));
numBatches = modelBuilder.getConfigOrDefault("batches", numBatches);
setPatience(modelBuilder.getConfigOrDefault("patience", patience));
return this;
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A native Java library for Graph Neural Networks.

# :cyclone: Changes from 1.0.0

* Neuralang: a scripting language for neural network declaration
* [Neuralang](Neuralang.md): a scripting language for neural network declaration
* Autosized parameteters
* Up to 30% less memory and running time
* Renamed `GCNBuilder` to `FastBuilder`
Expand Down
3 changes: 1 addition & 2 deletions architectures.nn
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ fn classify(nodes, h, epochs: 3000, patience: 100, lr: 0.01) {
return softmax(h[nodes], row);
}

fn gcnlayer(A, h, hidden: 64, reg: 0.005) {
fn gcnlayer(A, h, hidden: 16, reg: 0.005) {
return relu(A@h@matrix(?, hidden, reg) + vector(hidden));
}

Expand All @@ -11,4 +11,3 @@ fn gcn(A, h, classes: extern) {
h = gcnlayer(A, h, hidden: classes);
return h;
}

75 changes: 72 additions & 3 deletions tutorials/Nueralang.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,74 @@
# Neuralang

This is a scripting language defined by JGNN that defines graph and traditional
neural network architectures. It extends the library's main symbolic definition
capabilities with function declarations.
This is a scripting language for defining graph and traditional
neural network architectures. It extends JGNN's symbolic definition
with function declarations that construct the underlying execution
graph.



## Script

Neuralang scripts consist of function declarations like the ones bellow.
These scripts define neural network components and their interactions
using a syntax inspired by Mojo. Use a Rust highlighter, which covers
all keywords. Below are examples of function declarations:

```rust
fn classify(nodes, h, epochs: 3000, patience: 100, lr: 0.01) {
return softmax(h[nodes], row);
}
```

The classify function takes several parameters: nodes, which are the input nodes for classification; h, the feature matrix; epochs, which defaults to 3000 and represents the number of training epochs; patience, which defaults to 100 and denotes the early stopping patience; and lr, the learning rate, which defaults to 0.01. The function returns the softmax output for the specified nodes. Configuration defaults are indicated by a colon (:) in the function signatures.

```rust
fn gcnlayer(A, h, hidden: 64, reg: 0.005) {
return relu(A@h@matrix(?, hidden, reg) + vector(hidden));
}
```

The gcnlayer function accepts the following parameters: A is the adjacency matrix; h is the input feature matrix; hidden is a config that defaults to 64 and specifies the number of hidden units; and reg is the regularization term that defaults to 0.005. The ? in matrix definitions lets the autosize feature of Java integration determine the dimension name based on a test run, which uses an empty tensor to avoid computations. The function returns the activated output of the GCN layer using ReLU.

```rust
fn gcn(A, h, classes: extern) {
h = gcnlayer(A, h);
h = gcnlayer(A, h, hidden: classes);
return h;
}
```

The gcn function declares the popular Graph Convoluational Network (GCN) architecture. It takes these parameters: A is the adjacency matrix; h is the input feature matrix; and classes is the number of output classes. The function first applies a gcnlayer to A and h, and then applies another layer of the same type with the hidden units config set to the value of classes, to make the output match the number of classes. The number of classes is a config that should always be externally declared (there is no default), as indicated by its value being the extend keyword.

Config values have the priority: values provided during function calls > Java configs > defaults specified in function signatures.


## Java integration

Neuralang scripts can be integrated into Java code for building and training models. Below is an example of how to do so:


```java
Dataset dataset = new Cora();
dataset.graph().setMainDiagonal(1).setToSymmetricNormalization();

ModelBuilder modelBuilder = new Neuralang()
.parse(Paths.get("../architectures.nn"))
.constant("A", dataset.graph())
.constant("h", dataset.features())
.var("nodes")
.config("classes", dataset.labels().getCols())
.config("hidden", 16)
.out("classify(nodes, gcn(A,h))")
.autosize(new EmptyTensor(dataset.samples().getSlice().size()));

ModelTraining trainer = new ModelTraining()
.configFrom(modelBuilder)
.setVerbose(true)
.setLoss(new CategoricalCrossEntropy())
.setValidationLoss(new CategoricalCrossEntropy());
```

In this example, a dataset (Cora) is loaded, and its graph is prepared by setting the main diagonal and normalizing symmetrically. A Neuralang instance is created, which is a ModelBuilder that can parse scripts. Constants like the adjacency matrix A and feature matrix h are set, along with variables (nodes) and configurations (classes, hidden). The output of the model is defined with a Neuralang statement. Finally, dimension names and sizes for ? found model declaration are filled by calling autosize. This uses an empty tensor to avoid computations and determine the necessary dimensions.

A ModelTraining instance is then configured using parameters from the ModelBuilder, utilizing the configurations found in the classification method.

0 comments on commit 4fbd138

Please sign in to comment.