Skip to content

Commit 57d0433

Browse files
authored
Release 0.1.0/sparsify 2019 02 18 (#11)
* Attempt to test for #4 PyTorch's boolean comparison crap isn't useful and makes it a pain to test exact tensor values. * Will resume later * Skipping sparsify test It's a painfully simple function that has worked every time I've used it. - No it doesn't handle every edge case + Yes, it gets the job done and can be packaged for the general case * Use instance `.nonzero()` instead of `torch.nonzero()` * Fix "type-check" in layer inspectors * WIP: Implement shrink() in terms of resize_layers() It was as easy as I wanted it to be. * The complexity is how to handle a given nested layer + Those will get implemented with a given feature - Need to program feature detection TODO: + Implement the resizing on a layer-by-layer case, to make the shrinking a bit different + Instead of applying the data transformation uniformly, each layer gets + Those factors will be computed as 1 - percent_waste(layer) * Lay out skeleton for the true shrinking algo #4 * shrink_layer() is simple * Justification for giving Shrinkage a 'input_dimensions' property: > The thought is that channel depth doesn't change the output dimensions for CNNs, and that's attribute we're concerned with in the convulotional case... * Linear layers only have two dimensions, so it's a huge deal there. * RNNs do linear things over 'timesteps', so it's a big deal there. * Residual/identity/skip-connections in CNNs need this. > __It's decided__. The attribute stays
1 parent 24bb995 commit 57d0433

File tree

7 files changed

+313
-22
lines changed

7 files changed

+313
-22
lines changed

check-prune-widen.ipynb

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 2,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"import morph"
10+
]
11+
},
12+
{
13+
"cell_type": "code",
14+
"execution_count": 3,
15+
"metadata": {},
16+
"outputs": [
17+
{
18+
"data": {
19+
"text/plain": [
20+
"<module 'morph.nn' from '/Users/stephen/Documents/Insight-AI/Insight-AI-Fellowship-Project/src/morph/nn/__init__.py'>"
21+
]
22+
},
23+
"execution_count": 3,
24+
"metadata": {},
25+
"output_type": "execute_result"
26+
}
27+
],
28+
"source": [
29+
"morph.nn"
30+
]
31+
},
32+
{
33+
"cell_type": "code",
34+
"execution_count": 4,
35+
"metadata": {},
36+
"outputs": [],
37+
"source": [
38+
"??morph.nn.once"
39+
]
40+
},
41+
{
42+
"cell_type": "code",
43+
"execution_count": 5,
44+
"metadata": {},
45+
"outputs": [],
46+
"source": [
47+
"import morph.nn.shrink as ms"
48+
]
49+
},
50+
{
51+
"cell_type": "code",
52+
"execution_count": 6,
53+
"metadata": {},
54+
"outputs": [],
55+
"source": [
56+
"from morph.testing.models import EasyMnist"
57+
]
58+
},
59+
{
60+
"cell_type": "code",
61+
"execution_count": 7,
62+
"metadata": {},
63+
"outputs": [
64+
{
65+
"data": {
66+
"text/plain": [
67+
"0"
68+
]
69+
},
70+
"execution_count": 7,
71+
"metadata": {},
72+
"output_type": "execute_result"
73+
}
74+
],
75+
"source": [
76+
"some_linear = ms.nn.Linear(3, 2)\n",
77+
"c = [c for c in some_linear.children()]\n",
78+
"len(c)"
79+
]
80+
},
81+
{
82+
"cell_type": "code",
83+
"execution_count": 9,
84+
"metadata": {},
85+
"outputs": [
86+
{
87+
"data": {
88+
"text/plain": [
89+
"EasyMnist(\n",
90+
" (linear1): Linear(in_features=784, out_features=1000, bias=True)\n",
91+
" (linear2): Linear(in_features=1000, out_features=30, bias=True)\n",
92+
" (linear3): Linear(in_features=30, out_features=10, bias=True)\n",
93+
")"
94+
]
95+
},
96+
"execution_count": 9,
97+
"metadata": {},
98+
"output_type": "execute_result"
99+
}
100+
],
101+
"source": [
102+
"EasyMnist()"
103+
]
104+
},
105+
{
106+
"cell_type": "code",
107+
"execution_count": 8,
108+
"metadata": {},
109+
"outputs": [
110+
{
111+
"data": {
112+
"text/plain": [
113+
"Module(\n",
114+
" (linear1): Linear(in_features=784, out_features=700, bias=True)\n",
115+
" (linear2): Linear(in_features=700, out_features=21, bias=True)\n",
116+
" (linear3): Linear(in_features=21, out_features=10, bias=True)\n",
117+
")"
118+
]
119+
},
120+
"execution_count": 8,
121+
"metadata": {},
122+
"output_type": "execute_result"
123+
}
124+
],
125+
"source": [
126+
"ms.prune(EasyMnist())"
127+
]
128+
},
129+
{
130+
"cell_type": "code",
131+
"execution_count": null,
132+
"metadata": {},
133+
"outputs": [],
134+
"source": []
135+
}
136+
],
137+
"metadata": {
138+
"kernelspec": {
139+
"display_name": "Python 3",
140+
"language": "python",
141+
"name": "python3"
142+
},
143+
"language_info": {
144+
"codemirror_mode": {
145+
"name": "ipython",
146+
"version": 3
147+
},
148+
"file_extension": ".py",
149+
"mimetype": "text/x-python",
150+
"name": "python",
151+
"nbconvert_exporter": "python",
152+
"pygments_lexer": "ipython3",
153+
"version": "3.7.2"
154+
},
155+
"toc": {
156+
"base_numbering": 1,
157+
"nav_menu": {},
158+
"number_sections": true,
159+
"sideBar": true,
160+
"skip_h1_title": false,
161+
"title_cell": "Table of Contents",
162+
"title_sidebar": "Contents",
163+
"toc_cell": false,
164+
"toc_position": {},
165+
"toc_section_display": true,
166+
"toc_window_display": false
167+
}
168+
},
169+
"nbformat": 4,
170+
"nbformat_minor": 2
171+
}

morph/layers/sparse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def percent_waste(layer: nn.Module) -> float:
2828
weight matrix/tensor to determine how many neurons can be spared
2929
"""
3030
w = layer.weight
31-
non_sparse_w = torch.nonzero(sparsify(w))
31+
non_sparse_w = sparsify(w).nonzero()
3232
non_zero_count = non_sparse_w.numel() // len(non_sparse_w[0])
3333

3434
percent_size = non_zero_count / w.numel()

morph/layers/sparse_test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from unittest import main as test_main, TestCase, skip
2+
3+
from .sparse import sparsify, torch
4+
5+
class TestSparseFunctions(TestCase):
6+
7+
@skip("Skipping value-wise comparison until better solution than iterating all tensor values")
8+
def test_sparsify_selected_indices_should_have_sub_threshold_values(self):
9+
test_threshold = 0.1
10+
test_tensor = torch.randn(3, 2)
11+
expected = torch.where(test_tensor > test_threshold, test_tensor, torch.zeros(3, 2))
12+
self.assertEqual(expected, sparsify(test_tensor, test_threshold))
13+
14+
15+
if __name__ == "__main__":
16+
test_main()

morph/nn/resizing.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from collections import namedtuple
2+
3+
Resizing = namedtuple('Resizing', ['input_size', 'output_size'], defaults=[0, 0])
4+
Resizing.__doc__ += ': Baseclass for a type that encapsulates a resized layer'
5+
Resizing.input_size.__doc__ = "The layer's \"new\" input dimension size (Linear -> in_features, Conv2d -> in_channels)"
6+
Resizing.output_size.__doc__ = "The layer's \"new\" output dimension size (Linear -> out_features, Conv2d -> out_channels)"

morph/nn/shrink.py

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,103 @@
11
from morph.layers.sparse import percent_waste
2-
from morph._utils import check, round
3-
from morph.nn.utils import in_dim, out_dim
2+
from morph.utils import check, round
3+
from .resizing import Resizing
4+
from .utils import in_dim, out_dim, group_layers_by_algo
5+
from .widen import resize_layers
6+
from ._types import type_name
7+
8+
from typing import List
49

510
import torch.nn as nn
611

712

8-
def calc_reduced_size(layer: nn.Module) -> (int, int):
9-
"""Calculates the reduced size of the layer, post training (initial or morphed re-training)
10-
so the layers can be resized.
13+
class Shrinkage:
14+
"""
15+
An intermediary for the "Shrink" step of the three step Morphing algorithm.
16+
Rather than have all of the state be free in the small scope of a mega-function,
17+
these abstractions ease the way of implementing the shrinking and prune of the
18+
network.
19+
* Given that we have access to the total count of nodes, and how wasteful a layer was
20+
we can deduce any necessary changes once given a new input dimension
21+
* We expect input dimensions to change to accomodate the trimmed down earlier layers,
22+
but we want an expansion further along to allow the opening of bottlenecks in the architecture
23+
"""
24+
25+
def __init__(self, input_dimension: int, initial_parameters: int,
26+
waste_percentage: float):
27+
self.input_dimension = input_dimension # TODO: is this relevant in any non-Linear case?
28+
self.initial_parameters = initial_parameters
29+
self.waste_percentage = waste_percentage
30+
self.reduced_parameters = Shrinkage.reduce_parameters(initial_parameters,
31+
waste_percentage)
32+
33+
@staticmethod
34+
def reduce_parameters(initial_parameters: int, waste: float) -> int:
35+
"""Calculates the new, smaller, number of paratemers that this instance encapsulates"""
36+
percent_keep = (1. - waste)
37+
unrounded_params_to_keep = percent_keep * initial_parameters
38+
# round digital up to the nearest integer
39+
return round(unrounded_params_to_keep)
40+
41+
42+
def shrink_to_resize(shrinkage: Shrinkage, new_input_dimension: int) -> Resizing:
43+
"""Given the `new_input_dimension`, calculate a reshaping/resizing for the parameters
44+
of the supplied `shrinkage`.
45+
We round up the new output dimension, generously allowing for opening bottlenecks.
46+
Iteratively, any waste introduced is pruned hereafter. (Needs proof/unit test)
1147
"""
12-
# TODO: remove this guard when properly we protect access to this function
13-
check(
14-
type(layer) == nn.Conv2d or type(layer) == nn.Linear,
15-
'Invalid layer type: ' + type(layer))
48+
new_output_dimension = round(shrinkage.reduced_parameters / new_input_dimension)
49+
return Resizing(new_input_dimension, new_output_dimension)
50+
51+
52+
#################### prove of a good implementation ####################
53+
54+
55+
def uniform_prune(net: nn.Module) -> nn.Module:
56+
"""Shrink the network down 70%. Input and output dimensions are not altered"""
57+
return resize_layers(net, width_factor=0.7)
58+
59+
60+
#################### the algorithm to end all algorithms ####################
61+
62+
63+
def shrink_layer(layer: nn.Module) -> Shrinkage:
64+
waste = percent_waste(layer)
65+
parameter_count = layer.weight.numel() # the count is already tracked for us
66+
return Shrinkage(in_dim(layer), parameter_count, waste)
67+
68+
69+
def fit_layer_sizes(layer_sizes: List[Shrinkage]) -> List[Resizing]:
70+
# TODO: where's the invocation site for shrink_to_resize
71+
pass
72+
73+
74+
def transform(original_layer: nn.Module, new_shape: Resizing) -> nn.Module:
75+
# TODO: this might just be utils.redo_layer, without the primitive obsession
76+
pass
77+
78+
79+
def shrink_prune_fit(net: nn.Module) -> nn.Module:
80+
first, middle_layers, last = group_layers_by_algo(net)
81+
shrunk = {
82+
"first": shrink_layer(first),
83+
"middle": [shrink_layer(m) for m in middle_layers],
84+
"last": shrink_layer(last)
85+
}
86+
87+
# FIXME: why doesn't the linter like `fitted_layers`
88+
fitted_layers = fit_layer_sizes([shrunk["first"], *shrunk["middle"], shrunk["last"]])
89+
90+
# iteration very similar to `resize_layers` but matches Shrinkage with the corresponding layer
91+
new_first, new_middle_layers, new_last = group_layers_by_algo(fitted_layers)
92+
93+
new_net = nn.Module()
94+
95+
new_net.add_module(type_name(first), transform(first, new_first))
96+
97+
for old, new in zip(middle_layers, new_middle_layers):
98+
new_net.add_module(type_name(old), transform(old, new))
99+
pass # append to new_net with the Shrinkage's properties
16100

17-
percent_keep = 1 - percent_waste(layer)
18-
shrunk_in, shrunk_out = percent_keep * in_dim(layer), percent_keep * out_dim(layer)
101+
new_net.add_module(type_name(last), transform(last, new_last))
19102

20-
return round(shrunk_in), round(shrunk_out)
103+
return new_net

morph/nn/utils.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,26 @@ def make_children_list(children_or_named_children):
4747

4848

4949
def in_dim(layer: nn.Module) -> int:
50-
check(type_supported(layer))
50+
"""Returns the input dimension of a given (supported) `layer`"""
51+
layer_name = type_name(layer)
52+
check(type_supported(layer_name))
5153

52-
if layer_is_linear(layer):
54+
if layer_is_linear(layer_name):
5355
return layer.in_features
54-
elif layer_is_conv2d(layer):
56+
elif layer_is_conv2d(layer_name):
5557
return layer.in_channels
5658
else:
5759
raise RuntimeError('Inspecting on unsupported layer')
5860

5961

6062
def out_dim(layer: nn.Module) -> int:
61-
check(type_supported(layer))
63+
"""Returns the output dimension of a given (supported) `layer`"""
64+
layer_name = type_name(layer)
65+
check(type_supported(layer_name))
6266

63-
if layer_is_linear(layer):
67+
if layer_is_linear(layer_name):
6468
return layer.out_features
65-
elif layer_is_conv2d(layer):
69+
elif layer_is_conv2d(layer_name):
6670
return layer.out_channels
6771
else:
6872
raise RuntimeError('Inspecting on unsupported layer')

0 commit comments

Comments
 (0)