Skip to content

Commit a5dfe1a

Browse files
Xharktensorflower-gardener
authored andcommitted
Initial API for specifying 'compressible_weights'.
For testing, Added bias_only algorithm that compress bias vector has same weight for each layer. PiperOrigin-RevId: 339374777
1 parent 16aa75b commit a5dfe1a

File tree

7 files changed

+357
-25
lines changed

7 files changed

+357
-25
lines changed

tensorflow_model_optimization/python/core/common/keras/compression/algorithm.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,23 @@ def training(self, *training_weights: tf.Tensor) -> tf.Tensor:
110110
tf.Tensor to set the compressible weight to.
111111
"""
112112

113+
# TODO(tfmot): Consider separate from algorithm API for custom layer supports.
114+
def get_compressible_weights(
115+
self, original_layer: tf.keras.layers.Layer) -> List[str]:
116+
"""Define compressible weights for each layer.
117+
118+
Args:
119+
original_layer: tf.keras.layers.Layer representing a layer from the
120+
original model.
121+
122+
Returns:
123+
List of atrribute names as string representing list of compressible
124+
weights for the given layer. (e.g. return value ['kernel'] means
125+
layer.kernel is compressible.)
126+
"""
127+
del original_layer
128+
return []
129+
113130

114131
def create_layer_for_training(
115132
layer: tf.keras.layers.Layer,

tensorflow_model_optimization/python/core/common/keras/compression/algorithms/BUILD

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,25 @@ py_test(
4545
# tensorflow dep1,
4646
],
4747
)
48+
49+
py_library(
50+
name = "bias_only",
51+
srcs = ["bias_only.py"],
52+
srcs_version = "PY3",
53+
deps = [
54+
# tensorflow dep1,
55+
"//tensorflow_model_optimization/python/core/common/keras/compression:algorithm",
56+
],
57+
)
58+
59+
py_test(
60+
name = "bias_only_test",
61+
timeout = "long",
62+
srcs = ["bias_only_test.py"],
63+
python_version = "PY3",
64+
deps = [
65+
":bias_only",
66+
# numpy dep1,
67+
# tensorflow dep1,
68+
],
69+
)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Copyright 2020 The TensorFlow Authors. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
# ==============================================================================
15+
"""BiasOnly algorithm, where the compress bias only."""
16+
from typing import List
17+
18+
import tensorflow as tf
19+
20+
from tensorflow_model_optimization.python.core.common.keras.compression import algorithm
21+
22+
23+
# TODO(tfmot): This algorithm is showcase for bias only compression. if we find
24+
# better algorithm that can show better compressible weights coverage, then
25+
# we can remove this algorithm.
26+
class BiasOnly(algorithm.WeightCompressionAlgorithm):
27+
"""Define how to apply BiasOnly algorithm."""
28+
29+
# TODO(tfmot): communicate that `pretrained_weight` will sometimes
30+
# be a dummy tensor and sometimes be actual pretrained values during
31+
# its actual usage.
32+
def init_training_weights_repr(
33+
self, pretrained_weight: tf.Tensor) -> List[algorithm.WeightRepr]:
34+
bias_mean = tf.reduce_mean(pretrained_weight)
35+
bias_shape = tf.shape(pretrained_weight)
36+
37+
# TODO(tfmot): note that it does not suffice to just have the initializer
38+
# to derive the shape from, in the case of a constant initializer.
39+
# The unit test fail without providing the shape.
40+
return [
41+
algorithm.WeightRepr(
42+
name='bias_mean',
43+
shape=(),
44+
initializer=tf.keras.initializers.Constant(bias_mean)),
45+
algorithm.WeightRepr(
46+
name='bias_shape',
47+
shape=bias_shape.shape,
48+
dtype=bias_shape.dtype,
49+
initializer=tf.keras.initializers.Constant(bias_shape))
50+
]
51+
52+
def decompress(
53+
self, bias_mean: tf.Tensor, bias_shape: tf.Tensor) -> tf.Tensor:
54+
return tf.broadcast_to(bias_mean, bias_shape)
55+
56+
def training(
57+
self, bias_mean: tf.Tensor, bias_shape: tf.Tensor) -> tf.Tensor:
58+
return self.decompress(bias_mean, bias_shape)
59+
60+
def get_compressible_weights(
61+
self, original_layer: tf.keras.layers.Layer) -> List[str]:
62+
if isinstance(original_layer, tf.keras.layers.Conv2D) or \
63+
isinstance(original_layer, tf.keras.layers.Dense):
64+
return ['bias']
65+
return []
66+
67+
68+
def optimize(to_optimize: tf.keras.Model) -> tf.keras.Model:
69+
"""Model developer API for optimizing a model."""
70+
71+
def _optimize_layer(layer):
72+
# Require layer to be built so that the average of bias can be initialized.
73+
if not layer.built:
74+
raise ValueError(
75+
'Applying BiasOnly currently requires passing in a built model')
76+
77+
return algorithm.create_layer_for_training(layer, algorithm=BiasOnly())
78+
79+
return tf.keras.models.clone_model(
80+
to_optimize, clone_function=_optimize_layer)
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# Copyright 2020 The TensorFlow Authors. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
# ==============================================================================
15+
"""Tests for bias only optimization."""
16+
17+
import os
18+
import tempfile
19+
20+
import numpy as np
21+
import tensorflow as tf
22+
23+
from tensorflow_model_optimization.python.core.common.keras.compression.algorithms import bias_only
24+
25+
26+
def _build_model():
27+
i = tf.keras.layers.Input(shape=(28, 28), name='input')
28+
x = tf.keras.layers.Reshape((28, 28, 1))(i)
29+
x = tf.keras.layers.Conv2D(
30+
20, 5, activation='relu', padding='valid', name='conv1')(
31+
x)
32+
x = tf.keras.layers.MaxPool2D(2, 2)(x)
33+
x = tf.keras.layers.Conv2D(
34+
50, 5, activation='relu', padding='valid', name='conv2')(
35+
x)
36+
x = tf.keras.layers.MaxPool2D(2, 2)(x)
37+
x = tf.keras.layers.Flatten()(x)
38+
x = tf.keras.layers.Dense(500, activation='relu', name='fc1')(x)
39+
output = tf.keras.layers.Dense(10, name='fc2')(x)
40+
41+
model = tf.keras.Model(inputs=[i], outputs=[output])
42+
return model
43+
44+
45+
def _get_dataset():
46+
mnist = tf.keras.datasets.mnist
47+
(x_train, y_train), (x_test, y_test) = mnist.load_data()
48+
x_train, x_test = x_train / 255.0, x_test / 255.0
49+
# Use subset of 60000 examples to keep unit test speed fast.
50+
x_train = x_train[0:1000]
51+
y_train = y_train[0:1000]
52+
return (x_train, y_train), (x_test, y_test)
53+
54+
55+
def _train_model(model):
56+
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
57+
58+
model.compile(optimizer='adam', loss=loss_fn, metrics=['accuracy'])
59+
60+
(x_train, y_train), _ = _get_dataset()
61+
62+
model.fit(x_train, y_train, epochs=1)
63+
64+
65+
def _save_as_saved_model(model):
66+
saved_model_dir = tempfile.mkdtemp()
67+
model.save(saved_model_dir)
68+
return saved_model_dir
69+
70+
71+
# TODO(tfmot): reuse existing test utilities.
72+
def _convert_to_tflite(saved_model_dir):
73+
_, tflite_file = tempfile.mkstemp()
74+
75+
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
76+
tflite_model = converter.convert()
77+
78+
with open(tflite_file, 'wb') as f:
79+
f.write(tflite_model)
80+
81+
return tflite_file
82+
83+
84+
# TODO(tfmot): reuse test_utils_mnist.py.
85+
def _test_tflite(tflite_file):
86+
interpreter = tf.lite.Interpreter(model_path=tflite_file)
87+
interpreter.allocate_tensors()
88+
89+
input_index = interpreter.get_input_details()[0]['index']
90+
output_index = interpreter.get_output_details()[0]['index']
91+
92+
(_, _), (x_test, y_test) = _get_dataset()
93+
94+
# Testing the entire dataset is too slow. Verifying only 300 of 10k samples.
95+
x_test = x_test[0:300, :]
96+
y_test = y_test[0:300]
97+
98+
total_seen = 0
99+
num_correct = 0
100+
101+
for img, label in zip(x_test, y_test):
102+
batch_input_shape = (1, 28, 28)
103+
inp = img.reshape(batch_input_shape)
104+
inp = inp.astype(np.float32)
105+
total_seen += 1
106+
interpreter.set_tensor(input_index, inp)
107+
interpreter.invoke()
108+
predictions = interpreter.get_tensor(output_index)
109+
110+
if np.argmax(predictions) == label:
111+
num_correct += 1
112+
113+
return float(num_correct) / float(total_seen)
114+
115+
116+
def _get_directory_size_in_bytes(directory):
117+
total = 0
118+
try:
119+
for entry in os.scandir(directory):
120+
if entry.is_file():
121+
# if it's a file, use stat() function
122+
total += entry.stat().st_size
123+
elif entry.is_dir():
124+
# if it's a directory, recursively call this function
125+
total += _get_directory_size_in_bytes(entry.path)
126+
except NotADirectoryError:
127+
# if `directory` isn't a directory, get the file size then
128+
return os.path.getsize(directory)
129+
except PermissionError:
130+
# if for whatever reason we can't open the folder, return 0
131+
return 0
132+
return total
133+
134+
135+
class FunctionalTest(tf.test.TestCase):
136+
137+
def testBiasOnly_ReducesParamaters(self):
138+
model = _build_model()
139+
compressed_model = bias_only.optimize(model)
140+
141+
self.assertEqual(model.count_params(), 431080)
142+
self.assertEqual(compressed_model.count_params(), 430508)
143+
144+
def testBiasOnly_HasReasonableAccuracy_TF(self):
145+
model = _build_model()
146+
147+
compressed_model = bias_only.optimize(model)
148+
149+
_train_model(compressed_model)
150+
151+
_, (x_test, y_test) = _get_dataset()
152+
153+
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
154+
155+
compressed_model.compile(
156+
optimizer='adam', loss=loss_fn, metrics=['accuracy'])
157+
158+
results = compressed_model.evaluate(x_test, y_test)
159+
160+
self.assertGreater(results[1], 0.60)
161+
162+
def testBiasOnly_HasReasonableAccuracy_TFLite(self):
163+
model = _build_model()
164+
165+
compressed_model = bias_only.optimize(model)
166+
167+
_train_model(compressed_model)
168+
169+
saved_model_dir = _save_as_saved_model(compressed_model)
170+
compressed_tflite_file = _convert_to_tflite(saved_model_dir)
171+
172+
accuracy = _test_tflite(compressed_tflite_file)
173+
174+
self.assertGreater(accuracy, 0.60)
175+
176+
# TODO(tfmot): can simplify to single layer test.
177+
def testBiasOnly_BreaksDownLayerWeights(self):
178+
model = _build_model()
179+
180+
first_conv_layer = model.layers[2]
181+
self.assertLen(first_conv_layer.weights, 2)
182+
183+
compressed_model = bias_only.optimize(model)
184+
185+
first_conv_layer = compressed_model.layers[2]
186+
187+
self.assertLen(first_conv_layer.weights, 3)
188+
189+
# TODO(tfmot): can simplify to single layer test.
190+
def testBiasOnly_PreservesPretrainedWeights(self):
191+
i = tf.keras.layers.Input(shape=(2), name='input')
192+
output = tf.keras.layers.Dense(3, name='fc1')(i)
193+
model = tf.keras.Model(inputs=[i], outputs=[output])
194+
195+
dense_layer_weights = model.layers[1].get_weights()
196+
197+
compressed_model = bias_only.optimize(model)
198+
199+
dense_layer_compressed_weights = compressed_model.layers[1].get_weights()
200+
201+
# kernel
202+
assert (dense_layer_weights[0] == dense_layer_compressed_weights[2]).all()
203+
204+
# bias
205+
algorithm = bias_only.BiasOnly()
206+
w1_repr, w2_repr = algorithm.init_training_weights_repr(
207+
dense_layer_weights[1])
208+
209+
w1 = w1_repr.initializer(shape=None, dtype=w1_repr.dtype)
210+
w2 = w2_repr.initializer(shape=None, dtype=w2_repr.dtype)
211+
212+
assert (w1 == dense_layer_compressed_weights[0]).numpy().all()
213+
assert (w2 == dense_layer_compressed_weights[1]).numpy().all()
214+
215+
216+
if __name__ == '__main__':
217+
tf.test.main()

tensorflow_model_optimization/python/core/common/keras/compression/algorithms/different_training_and_inference.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ def compress(self, weight: tf.Tensor) -> List[tf.Tensor]:
7373
def training(self, weight: tf.Tensor) -> tf.Tensor:
7474
return weight
7575

76+
def get_compressible_weights(
77+
self, original_layer: tf.keras.layers.Layer) -> List[str]:
78+
if isinstance(original_layer, tf.keras.layers.Conv2D) or \
79+
isinstance(original_layer, tf.keras.layers.Dense):
80+
return ['kernel']
81+
return []
82+
7683

7784
# TODO(tfmot): consider if we can simplify `create_model_for_training` and
7885
# `create_model_for_inference` into a single API for algorithm developers.

tensorflow_model_optimization/python/core/common/keras/compression/algorithms/same_training_and_inference.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ def decompress(self, u: tf.Tensor, sv: tf.Tensor) -> tf.Tensor:
7676
def training(self, u: tf.Tensor, sv: tf.Tensor) -> tf.Tensor:
7777
return self.decompress(u, sv)
7878

79+
def get_compressible_weights(
80+
self, original_layer: tf.keras.layers.Layer) -> List[str]:
81+
if isinstance(original_layer, tf.keras.layers.Conv2D) or \
82+
isinstance(original_layer, tf.keras.layers.Dense):
83+
return ['kernel']
84+
return []
85+
7986

8087
def optimize(to_optimize: tf.keras.Model, params: SVDParams) -> tf.keras.Model:
8188
"""Model developer API for optimizing a model."""

0 commit comments

Comments
 (0)