Skip to main content
Yanked

This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 0.11.1 instead.

Installation |Description |Quick Example |Filtering |StatefulComputation |Applications |More |Acknowledgements

Tests pyver codestyle Open In Colab Downloads codecov Documentation Status GitHub commit activity

🛠️ Installation

pip install pytreeclass

Install development version

pip install git+https://github.com/ASEM000/PyTreeClass

📖 Description

PyTreeClass is a JAX-compatible dataclass-like decorator to create and operate on stateful JAX PyTrees.

The package aims to achieve two goals:

  1. 🔒 To maintain safe and correct behaviour by using immutable modules with functional API.
  2. To achieve the most intuitive user experience in the JAX ecosystem by :
    • 🏗️ Defining layers similar to PyTorch or TensorFlow sublcassing style.
    • ☝️ Filtering\Indexing layer values by using boolean masking similar to jax.numpy.at[].{get,set,apply,...}
    • 🎨 Visualize defined layers in plethora of ways for better debugging and sharing of information

⏩ Quick Example

🏗️ Create simple MLP

import jax
from jax import numpy as jnp
import pytreeclass as pytc
import matplotlib.pyplot as plt

@pytc.treeclass
class Linear :
   # Any variable not wrapped with @pytc.treeclass
   # should be declared as a dataclass field here
   weight : jnp.ndarray
   bias   : jnp.ndarray

   def __init__(self,key,in_dim,out_dim):
       self.weight = jax.random.normal(key,shape=(in_dim, out_dim)) * jnp.sqrt(2/in_dim)
       self.bias = jnp.ones((1,out_dim))

   def __call__(self,x):
       return x @ self.weight + self.bias

@pytc.treeclass
class StackedLinear:

    def __init__(self,key,in_dim,out_dim,hidden_dim):
        keys= jax.random.split(key,3)

        # Declaring l1,l2,l3 as dataclass_fields is optional
        # as l1,l2,l3 are Linear class that is wrapped with @pytc.treeclass
        # To strictly include nodes defined in dataclass fields 
        # use `@pytc.treeclass(field_only=True)`
        self.l1 = Linear(key=keys[0],in_dim=in_dim,out_dim=hidden_dim)
        self.l2 = Linear(key=keys[1],in_dim=hidden_dim,out_dim=hidden_dim)
        self.l3 = Linear(key=keys[2],in_dim=hidden_dim,out_dim=out_dim)

    def __call__(self,x):
        x = self.l1(x)
        x = jax.nn.tanh(x)
        x = self.l2(x)
        x = jax.nn.tanh(x)
        x = self.l3(x)

        return x
        
model = StackedLinear(in_dim=1,out_dim=1,hidden_dim=10,key=jax.random.PRNGKey(0))

x = jnp.linspace(0,1,100)[:,None]
y = x**3 + jax.random.uniform(jax.random.PRNGKey(0),(100,1))*0.01

🎨 Visualize

summary tree_boxtree_diagram
print(model.summary())
┌────┬──────┬───────┬───────┬─────────────────┐
NameType  Param #│Size   │Config           │
├────┼──────┼───────┼───────┼─────────────────┤
l1  Linear20(0)  80.00B weight=f32[1,10] 
                 (0.00B)bias=f32[1,10]   
├────┼──────┼───────┼───────┼─────────────────┤
l2  Linear110(0) 440.00Bweight=f32[10,10]
                 (0.00B)bias=f32[1,10]   
├────┼──────┼───────┼───────┼─────────────────┤
l3  Linear11(0)  44.00B weight=f32[10,1] 
                 (0.00B)bias=f32[1,1]    
└────┴──────┴───────┴───────┴─────────────────┘
Total count :	141(0)
Dynamic count :	141(0)
Frozen count :	0(0)
-----------------------------------------------
Total size :	564.00B(0.00B)
Dynamic size :	564.00B(0.00B)
Frozen size :	0.00B(0.00B)
===============================================

using jax.eval_shape (no-flops operation)

note : the created modules in __init__ should be in the same order where they are called in __call__

print(model.tree_box(array=x))
┌──────────────────────────────────────┐
StackedLinear[Parent]                 
├──────────────────────────────────────┤
│┌────────────┬────────┬──────────────┐│
││             Input   f32[100,1]   ││
││ Linear[l1] │────────┼──────────────┤│
││             Output  f32[100,128] ││
│└────────────┴────────┴──────────────┘│
│┌────────────┬────────┬──────────────┐│
││             Input   f32[100,128] ││
││ Linear[l2] │────────┼──────────────┤│
││             Output  f32[100,128] ││
│└────────────┴────────┴──────────────┘│
│┌────────────┬────────┬──────────────┐│
││             Input   f32[100,128] ││
││ Linear[l3] │────────┼──────────────┤│
││             Output  f32[100,1]   ││
│└────────────┴────────┴──────────────┘│
└──────────────────────────────────────┘
print(model.tree_diagram())
StackedLinear
    ├── l1=Linear
       ├── weight=f32[1,10]
       └── bias=f32[1,10]
    ├── l2=Linear
       ├── weight=f32[10,10]
       └── bias=f32[1,10]
    └──l3=Linear
        ├── weight=f32[10,1]
        └── bias=f32[1,1]
mermaid.io (Native support in Github/Notion)
# generate mermaid diagrams
# print(pytc.tree_viz.tree_mermaid(model)) # generate core syntax
pytc.tree_viz.save_viz(model,filename="test_mermaid",method="tree_mermaid_md")
# use `method="tree_mermaid_html"` to save as html
flowchart LR
    id15696277213149321320[StackedLinear]
    id15696277213149321320 --> id159132120600507116(l1\nLinear)
    id159132120600507116 --- id7500441386962467209["weight\nf32[1,10]"]
    id159132120600507116 --- id10793958738030044218["bias\nf32[1,10]"]
    id15696277213149321320 --> id10009280772564895168(l2\nLinear)
    id10009280772564895168 --- id11951215191344350637["weight\nf32[10,10]"]
    id10009280772564895168 --- id1196345851686744158["bias\nf32[1,10]"]
    id15696277213149321320 --> id7572222925824649475(l3\nLinear)
    id7572222925824649475 --- id4749243995442935477["weight\nf32[10,1]"]
    id7572222925824649475 --- id8042761346510512486["bias\nf32[1,1]"]
✨ Generate shareable vizualization links ✨
>>> pytc.tree_viz.tree_mermaid(model,link=True)
'Open URL in browser: https://pytreeclass.herokuapp.com/temp/?id=*********'

✂️ Model surgery

# freeze l1
model = model.at["l1"].freeze()

# Set negative_values in l2 to 0
filtered_l2 =  model.l2.at[model.l2<0].set(0) 
model = model.at["l2"].set( filtered_l2 )

# apply sin(x) to all values in l3
filtered_l3 = model.l3.at[...].apply(jnp.sin)
model  = model.at["l3"].set(filtered_l3)

# frozen nodes are marked with #
print(model.tree_diagram())
StackedLinear
    #─ l1=Linear
       #─ weight=f32[1,10]
       #─ bias=f32[1,10]  
    ├── l2=Linear
       ├── weight=f32[10,10]
       └── bias=f32[1,10]  
    └── l3=Linear
        ├── weight=f32[10,1]
        └── bias=f32[1,1] 

☝️ Filtering with .at[]

PyTreeClass offers four means of filtering:

  1. Filter by value
  2. Filter by field name
  3. Filter by field type
  4. Filter by field metadata.

The following example demonstrates the usage the filtering. Suppose you have the following (Multilayer perceptron) MLP class

  • Note in StackedLinear l1 and l2 has a description in field metadata.
Model definition
import jax
from jax import numpy as jnp
import pytreeclass as pytc
import matplotlib.pyplot as plt
from dataclasses import  field 

@pytc.treeclass
class Linear :
   weight : jnp.ndarray
   bias   : jnp.ndarray

   def __init__(self,key,in_dim,out_dim):
       self.weight = jax.random.normal(key,shape=(in_dim, out_dim)) * jnp.sqrt(2/in_dim)
       self.bias = jnp.ones((1,out_dim))

   def __call__(self,x):
       return x @ self.weight + self.bias

@pytc.treeclass
class StackedLinear:
    l1 : Linear = field(metadata={"description": "First layer"})
    l2 : Linear = field(metadata={"description": "Second layer"})

    def __init__(self,key,in_dim,out_dim,hidden_dim):
        keys= jax.random.split(key,3)

        self.l1 = Linear(key=keys[0],in_dim=in_dim,out_dim=hidden_dim)
        self.l2 = Linear(key=keys[2],in_dim=hidden_dim,out_dim=out_dim)

    def __call__(self,x):
        x = self.l1(x)
        x = jax.nn.tanh(x)
        x = self.l2(x)

        return x
        
model = StackedLinear(in_dim=1,out_dim=1,hidden_dim=5,key=jax.random.PRNGKey(0))
  • Raw model values before any filtering.
print(model)
StackedLinear(
  l1=Linear(
    weight=[[-1.6248673  -2.8383057   1.3969219   1.3169124  -0.40784812]],
    bias=[[1. 1. 1. 1. 1.]]
  ),
  l2=Linear(
    weight=
      [[ 0.98507565]
       [ 0.99815285]
       [-1.0687716 ]
       [-0.19255024]
       [-1.2108876 ]],
    bias=[[1.]]
  )
)

Filter by value

  • Get all negative values
print(model.at[model<0].get())

StackedLinear(
  l1=Linear(
    weight=[-1.6248673  -2.8383057  -0.40784812],
    bias=[]
  ),
  l2=Linear(
    weight=[-1.0687716  -0.19255024 -1.2108876 ],
    bias=[]
  )
)
  • Set negative values to 0
print(model.at[model<0].set(0))

StackedLinear(
  l1=Linear(
    weight=[[0.        0.        1.3969219 1.3169124 0.       ]],
    bias=[[1. 1. 1. 1. 1.]]
  ),
  l2=Linear(
    weight=
      [[0.98507565]
       [0.99815285]
       [0.        ]
       [0.        ]
       [0.        ]],
    bias=[[1.]]
  )
)
  • Apply f(x)=x^2 to negative values
print(model.at[model<0].apply(lambda x:x**2))

StackedLinear(
  l1=Linear(
    weight=[[2.6401937  8.05598    1.3969219  1.3169124  0.16634008]],
    bias=[[1. 1. 1. 1. 1.]]
  ),
  l2=Linear(
    weight=
      [[0.98507565]
       [0.99815285]
       [1.1422727 ]
       [0.03707559]
       [1.4662486 ]],
    bias=[[1.]]
  )
)
  • Sum all negative values
print(model.at[model<0].reduce(lambda acc,cur: acc+jnp.sum(cur)))
-7.3432307

Filter by field name

  • Get all fields named l1
print(model.at[model == "l1"].get())

StackedLinear(
  l1=Linear(
    weight=[-1.6248673  -2.8383057   1.3969219   1.3169124  -0.40784812],
    bias=[1. 1. 1. 1. 1.]
  ),
  l2=Linear(weight=[],bias=[])
)

Filter by field type

  • Get all fields of Linear type
print(model.at[model == Linear].get())

StackedLinear(
  l1=Linear(
    weight=[-1.6248673  -2.8383057   1.3969219   1.3169124  -0.40784812],
    bias=[1. 1. 1. 1. 1.]
  ),
  l2=Linear(
    weight=[ 0.98507565  0.99815285 -1.0687716  -0.19255024 -1.2108876 ],
    bias=[1.]
  )
)

Filter by field metadata

  • Get all fields of with {"description": "First layer"} in their metadata
print(model.at[model == {"description": "First layer"}].get())

StackedLinear(
  l1=Linear(
    weight=[-1.6248673  -2.8383057   1.3969219   1.3169124  -0.40784812],
    bias=[1. 1. 1. 1. 1.]
  ),
  l2=Linear(weight=[],bias=[])
)

Mix and match different filtering methods.

  • Get only fields named weight positive values.
mask = (model == "weight") & (model>0)
print(model.at[mask].get())

StackedLinear(
  l1=Linear(weight=[1.3969219 1.3169124],bias=[]),
  l2=Linear(weight=[0.98507565 0.99815285],bias=[])
)

📝 Applications

🔢 More

More compact boilerplate

Using param:

  • More compact definition can be done with node defined at runtime call.
  • The Linear layers are defined on the first call and retrieved on the subsequent calls
  • This pattern is useful if the module definition depends on runtime data.
@pytc.treeclass
class StackedLinear:
    keys: Any

    def __init__(self,key):
        self.keys = jax.random.split(key,3)

    def __call__(self,x):
        x = self.param(Linear(self.keys[0],x.shape[-1],10),name="l1")(x)
        x = jax.nn.tanh(x)
        x = self.param(Linear(self.keys[1],10,10),name="l2")(x)
        x = jax.nn.tanh(x)
        x = self.param(Linear(self.keys[2],10,x.shape[-1]),name="l3")(x)
        return x
# Upon defining the layer the modules are not instantiated
# However, after first call , the nodes are defined.
model = StackedLinear(jax.random.PRNGKey(0))
print(model)
StackedLinear(keys=ui32[3,2])
model((jnp.ones((10,10)))) # first call
print(f"{model!r}")
StackedLinear(
  keys=ui32[3,2],
  l1=Linear(weight=f32[10,10],bias=f32[1,10]),
  l2=Linear(weight=f32[10,10],bias=f32[1,10]),
  l3=Linear(weight=f32[10,10],bias=f32[1,10])
)

📙 Acknowledgements

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytreeclass-0.1.0.tar.gz (44.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytreeclass-0.1.0-py3-none-any.whl (49.0 kB view details)

Uploaded Python 3

File details

Details for the file pytreeclass-0.1.0.tar.gz.

File metadata

  • Download URL: pytreeclass-0.1.0.tar.gz
  • Upload date:
  • Size: 44.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.10.6

File hashes

Hashes for pytreeclass-0.1.0.tar.gz
Algorithm Hash digest
SHA256 eb08e680dda6fda7a8e60e51d011912a9f57c3eed5420718c03a527cd7653c19
MD5 c8a77d2b7ce347382b398e6bb95b8b3e
BLAKE2b-256 84f2f166c547d2ac1e32dc53146f3f459bc514059b603ce9488018f7e82a87fe

See more details on using hashes here.

File details

Details for the file pytreeclass-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pytreeclass-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 49.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.10.6

File hashes

Hashes for pytreeclass-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f89a0561bbde753cc06a70517dcafd2470df61cb3f3f818aca6acd7eed0c25ca
MD5 0adcc6bee6ee3b15737f6e7d12c76dad
BLAKE2b-256 f1a0d41c9a69c045d5f59dd9989256c4e8deec17ef28027e719a0b8a60c376a8

See more details on using hashes here.

Release history Release notifications | RSS feed

0.11.1

2 files

0.11.0

2 files

0.9.2

2 files

0.9.1

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0.post0

2 files

0.6.0

2 files

0.5.0.post0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.8

2 files

0.3.7

2 files

0.3.6

2 files

0.3.4

2 files

0.3.3

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

0.2.8

2 files

0.2.7

2 files

0.2.6

2 files

0.2.5

2 files

0.2.4

2 files

0.2.3

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.13

2 files

0.1.12

2 files

0.1.11

2 files

0.1.10

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

This release

0.1.0 This release

2 files

0.0.11

2 files

0.0.9.post1

2 files

0.0.9

2 files

0.0.8

2 files

0.0.7

2 files

0.0.6.post2

2 files

0.0.6.post1

2 files

0.0.6.post0

2 files

0.0.6

2 files

0.0.5

2 files

0.0.4

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page