Skip to content
This repository was archived by the owner on Nov 7, 2024. It is now read-only.

Commit bb56981

Browse files
author
Ben Penchas
authored
Adding MPO layer (#576)
* Adding MPO layer * Improved network creation and contraction * Adding get_config to layers and updating tests * Adding SavedModel test and updating docstrings * Updating get_config to idiomatic Keras * Adding decorators for serializable layers * Fixing errors * Small fix * Decorator error * Investigating global custom objects error
1 parent cf2a024 commit bb56981

4 files changed

Lines changed: 389 additions & 50 deletions

File tree

tensornetwork/tn_keras/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
from tensornetwork.tn_keras.dense import DenseDecomp
1+
# from tensornetwork.tn_keras.dense import DenseDecomp
2+
# from tensornetwork.tn_keras.mpo import DenseMPO

tensornetwork/tn_keras/dense.py

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import numpy as np
88

99

10+
@tf.keras.utils.register_keras_serializable() # type: ignore
11+
# package='tensornetwork', name='dense_decomp')
1012
class DenseDecomp(Layer):
1113
"""TN layer comparable to Dense that carries out matrix multiplication
1214
with 2 significantly smaller weight matrices instead of 1 large one.
@@ -19,10 +21,10 @@ class DenseDecomp(Layer):
1921
# as first layer in a sequential model:
2022
model = Sequential()
2123
model.add(
22-
DenseDecomp(512, decomp_size=128, activation='relu', input_dim=1024))
24+
DenseDecomp(512, decomp_size=128, activation='relu', input_shape=(1024,)))
2325
# now the model will take as input arrays of shape (*, 1024)
24-
# and output arrays of shape (*, 512)
25-
# after the first layer, you don't need to specify
26+
# and output arrays of shape (*, 512).
27+
# After the first layer, you don't need to specify
2628
# the size of the input anymore:
2729
model.add(DenseDecomp(512, decomp_size=128, activation='relu'))
2830
```
@@ -39,7 +41,7 @@ class DenseDecomp(Layer):
3941
bias_initializer: Initializer for the bias vector.
4042
4143
Input shape:
42-
2D tensor with shape: `(batch_size, input_dim)`.
44+
2D tensor with shape: `(batch_size, input_shape[-1])`.
4345
4446
Output shape:
4547
2D tensor with shape: `(batch_size, output_dim)`.
@@ -53,16 +55,21 @@ def __init__(self,
5355
kernel_initializer: Optional[Text] = 'glorot_uniform',
5456
bias_initializer: Optional[Text] = 'zeros',
5557
**kwargs) -> None:
58+
59+
# Allow specification of input_dim instead of input_shape,
60+
# for compatability with Keras layers that support this
5661
if 'input_shape' not in kwargs and 'input_dim' in kwargs:
5762
kwargs['input_shape'] = (kwargs.pop('input_dim'),)
63+
64+
super(DenseDecomp, self).__init__(**kwargs)
65+
5866
self.output_dim = output_dim
5967
self.decomp_size = decomp_size
6068

6169
self.use_bias = use_bias
6270
self.activation = activations.get(activation)
6371
self.kernel_initializer = initializers.get(kernel_initializer)
6472
self.bias_initializer = initializers.get(bias_initializer)
65-
super(DenseDecomp, self).__init__(**kwargs)
6673

6774
def build(self, input_shape: List[int]) -> None:
6875
# Disable the attribute-defined-outside-init violations in this function
@@ -71,24 +78,24 @@ def build(self, input_shape: List[int]) -> None:
7178
raise ValueError('The last dimension of the inputs to `Dense` '
7279
'should be defined. Found `None`.')
7380

74-
self.a_var = self.add_weight(
75-
name='a',
76-
shape=(input_shape[-1], self.decomp_size),
77-
trainable=True,
78-
initializer=self.kernel_initializer)
79-
self.b_var = self.add_weight(
80-
name='b',
81-
shape=(self.decomp_size, self.output_dim),
82-
trainable=True,
83-
initializer=self.kernel_initializer)
81+
super(DenseDecomp, self).build(input_shape)
82+
83+
self.a_var = self.add_weight(name='a',
84+
shape=(input_shape[-1], self.decomp_size),
85+
trainable=True,
86+
initializer=self.kernel_initializer)
87+
self.b_var = self.add_weight(name='b',
88+
shape=(self.decomp_size, self.output_dim),
89+
trainable=True,
90+
initializer=self.kernel_initializer)
8491
self.bias_var = self.add_weight(
8592
name='bias',
8693
shape=(self.output_dim,),
8794
trainable=True,
8895
initializer=self.bias_initializer) if self.use_bias else None
89-
super(DenseDecomp, self).build(input_shape)
9096

91-
def call(self, inputs: tf.Tensor, **kwargs) -> tf.Tensor: # pylint: disable=unused-argument
97+
def call(self, inputs: tf.Tensor, **kwargs) -> tf.Tensor: # pylint: disable=unused-argument
98+
9299

93100
def f(x: tf.Tensor, a_var: tf.Tensor, b_var: tf.Tensor, use_bias: bool,
94101
bias_var: tf.Tensor) -> tf.Tensor:
@@ -124,3 +131,32 @@ def f(x: tf.Tensor, a_var: tf.Tensor, b_var: tf.Tensor, use_bias: bool,
124131

125132
def compute_output_shape(self, input_shape: List[int]) -> Tuple[int, int]:
126133
return (input_shape[0], self.output_dim)
134+
135+
def get_config(self) -> dict:
136+
"""Returns the config of the layer.
137+
138+
The same layer can be reinstantiated later
139+
(without its trained weights) from this configuration.
140+
141+
Returns:
142+
Python dictionary containing the configuration of the layer.
143+
"""
144+
config = {}
145+
146+
# Include the DenseDecomp-specific arguments
147+
decomp_args = ['output_dim', 'decomp_size', 'use_bias']
148+
for arg in decomp_args:
149+
config[arg] = getattr(self, arg)
150+
151+
# Serialize the activation
152+
config['activation'] = activations.serialize(getattr(self, 'activation'))
153+
154+
# Serialize the initializers
155+
decomp_initializers = ['kernel_initializer', 'bias_initializer']
156+
for initializer_arg in decomp_initializers:
157+
config[initializer_arg] = initializers.serialize(
158+
getattr(self, initializer_arg))
159+
160+
# Get base config
161+
base_config = super(DenseDecomp, self).get_config()
162+
return dict(list(base_config.items()) + list(config.items()))

tensornetwork/tn_keras/mpo.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import tensorflow as tf
2+
from tensorflow.keras.layers import Layer # type: ignore
3+
from tensorflow.keras import activations
4+
from tensorflow.keras import initializers
5+
from typing import List, Optional, Text, Tuple
6+
import tensornetwork as tn
7+
from tensornetwork.network_components import Node
8+
import numpy as np
9+
import math
10+
11+
12+
@tf.keras.utils.register_keras_serializable() # type: ignore
13+
# package='tensornetwork', name='dense_mpo')
14+
class DenseMPO(Layer):
15+
"""Matrix Product Operator (MPO) TN layer.
16+
17+
Example:
18+
19+
```python
20+
# as first layer in a sequential model:
21+
model = Sequential()
22+
model.add(
23+
DenseMPO(1024, num_nodes=4, bond_dim=8, activation='relu',
24+
input_shape=(1024,)))
25+
# now the model will take as input arrays of shape (*, 1024)
26+
# and output arrays of shape (*, 1024).
27+
# After the first layer, you don't need to specify
28+
# the size of the input anymore:
29+
model.add(DenseMPO(1024, num_nodes=4, bond_dim=8, activation='relu'))
30+
```
31+
32+
Args:
33+
output_dim: Positive integer, dimensionality of the output space.
34+
num_nodes: Positive integer, number of nodes in the MPO.
35+
Note input_shape[-1]**(1. / num_nodes) and output_dim**(1. / num_nodes)
36+
must both be round.
37+
bond_dim: Positive integer, size of the intermediate dimension.
38+
activation: Activation function to use.
39+
If you don't specify anything, no activation is applied
40+
(ie. "linear" activation: `a(x) = x`).
41+
use_bias: Boolean, whether the layer uses a bias vector.
42+
kernel_initializer: Initializer for the node weight matrices.
43+
bias_initializer: Initializer for the bias vector.
44+
45+
Input shape:
46+
2D tensor with shape: `(batch_size, input_shape[-1])`.
47+
48+
Output shape:
49+
2D tensor with shape: `(batch_size, output_dim)`.
50+
"""
51+
52+
def __init__(self,
53+
output_dim: int,
54+
num_nodes: int,
55+
bond_dim: int,
56+
use_bias: Optional[bool] = True,
57+
activation: Optional[Text] = None,
58+
kernel_initializer: Optional[Text] = 'glorot_uniform',
59+
bias_initializer: Optional[Text] = 'zeros',
60+
**kwargs) -> None:
61+
62+
# Allow specification of input_dim instead of input_shape,
63+
# for compatability with Keras layers that support this
64+
if 'input_shape' not in kwargs and 'input_dim' in kwargs:
65+
kwargs['input_shape'] = (kwargs.pop('input_dim'),)
66+
67+
assert num_nodes > 2, 'Need at least 2 nodes to create MPO'
68+
69+
super(DenseMPO, self).__init__(**kwargs)
70+
71+
self.output_dim = output_dim
72+
self.num_nodes = num_nodes
73+
self.bond_dim = bond_dim
74+
self.nodes = []
75+
self.use_bias = use_bias
76+
self.activation = activations.get(activation)
77+
self.kernel_initializer = initializers.get(kernel_initializer)
78+
self.bias_initializer = initializers.get(bias_initializer)
79+
80+
def build(self, input_shape: List[int]) -> None:
81+
# Disable the attribute-defined-outside-init violations in this function
82+
# pylint: disable=attribute-defined-outside-init
83+
if input_shape[-1] is None:
84+
raise ValueError('The last dimension of the inputs to `Dense` '
85+
'should be defined. Found `None`.')
86+
87+
def is_perfect_root(n, n_nodes):
88+
root = n**(1. / n_nodes)
89+
return round(root)**n_nodes == n
90+
91+
# Ensure the MPO dimensions will work
92+
assert is_perfect_root(input_shape[-1], self.num_nodes), \
93+
f'Input dim incorrect.\
94+
{input_shape[-1]}**(1. / {self.num_nodes}) must be round.'
95+
96+
assert is_perfect_root(self.output_dim, self.num_nodes), \
97+
f'Output dim incorrect. \
98+
{self.output_dim}**(1. / {self.num_nodes}) must be round.'
99+
100+
super(DenseMPO, self).build(input_shape)
101+
102+
self.in_leg_dim = math.ceil(input_shape[-1]**(1. / self.num_nodes))
103+
self.out_leg_dim = math.ceil(self.output_dim**(1. / self.num_nodes))
104+
105+
self.nodes.append(
106+
self.add_weight(name='end_node_first',
107+
shape=(self.in_leg_dim, self.bond_dim,
108+
self.out_leg_dim),
109+
trainable=True,
110+
initializer=self.kernel_initializer))
111+
for i in range(self.num_nodes - 2):
112+
self.nodes.append(
113+
self.add_weight(name=f'middle_node_{i}',
114+
shape=(self.in_leg_dim, self.bond_dim, self.bond_dim,
115+
self.out_leg_dim),
116+
trainable=True,
117+
initializer=self.kernel_initializer))
118+
self.nodes.append(
119+
self.add_weight(name='end_node_last',
120+
shape=(self.in_leg_dim, self.bond_dim,
121+
self.out_leg_dim),
122+
trainable=True,
123+
initializer=self.kernel_initializer))
124+
125+
self.bias_var = self.add_weight(
126+
name='bias',
127+
shape=(self.output_dim,),
128+
trainable=True,
129+
initializer=self.bias_initializer) if self.use_bias else None
130+
131+
def call(self, inputs: tf.Tensor, **kwargs) -> tf.Tensor: # pylint: disable=unused-argument
132+
133+
134+
def f(x: tf.Tensor, nodes: List[Node], num_nodes: int, in_leg_dim: int,
135+
use_bias: bool, bias_var: tf.Tensor) -> tf.Tensor:
136+
orig_shape = x.shape
137+
l = [in_leg_dim] * num_nodes
138+
input_reshaped = tf.reshape(x, tuple(l))
139+
x_node = tn.Node(input_reshaped, name='xnode', backend="tensorflow")
140+
141+
tn_nodes = []
142+
for i, v in enumerate(nodes):
143+
tn_nodes.append(tn.Node(v, name=f'node_{i}', backend="tensorflow"))
144+
# Connect every node to input node
145+
x_node[i] ^ tn_nodes[i][0]
146+
147+
# Connect all core nodes
148+
tn_nodes[0][1] ^ tn_nodes[1][1]
149+
for i, _ in enumerate(tn_nodes):
150+
if len(tn_nodes[i].shape) == 4:
151+
tn_nodes[i][2] ^ tn_nodes[i + 1][1]
152+
153+
# The TN should now look like this
154+
# | | |
155+
# 1 --- 2 --- ...
156+
# \ / /
157+
# x
158+
159+
# Contract TN using zipper algorithm
160+
temp = x_node @ tn_nodes[0]
161+
for i in range(1, len(tn_nodes)):
162+
temp = temp @ tn_nodes[i]
163+
164+
result = tf.reshape(temp.tensor, orig_shape)
165+
if use_bias:
166+
result += bias_var
167+
168+
return result
169+
170+
result = tf.vectorized_map(
171+
lambda vec: f(vec, self.nodes, self.num_nodes, self.in_leg_dim, self.
172+
use_bias, self.bias_var), inputs)
173+
if self.activation is not None:
174+
result = self.activation(result)
175+
return tf.reshape(result, (-1, self.output_dim))
176+
177+
def compute_output_shape(self, input_shape: List[int]) -> Tuple[int, int]:
178+
return (input_shape[0], self.output_dim)
179+
180+
def get_config(self) -> dict:
181+
"""Returns the config of the layer.
182+
183+
The same layer can be reinstantiated later
184+
(without its trained weights) from this configuration.
185+
186+
Returns:
187+
Python dictionary containing the configuration of the layer.
188+
"""
189+
config = {}
190+
191+
# Include the MPO-specific arguments
192+
mpo_args = ['output_dim', 'num_nodes', 'bond_dim', 'use_bias']
193+
for arg in mpo_args:
194+
config[arg] = getattr(self, arg)
195+
196+
# Serialize the activation
197+
config['activation'] = activations.serialize(getattr(self, 'activation'))
198+
199+
# Serialize the initializers
200+
mpo_initializers = ['kernel_initializer', 'bias_initializer']
201+
for initializer_arg in mpo_initializers:
202+
config[initializer_arg] = initializers.serialize(
203+
getattr(self, initializer_arg))
204+
205+
# Get base config
206+
base_config = super(DenseMPO, self).get_config()
207+
return dict(list(base_config.items()) + list(config.items()))

0 commit comments

Comments
 (0)