Skip to content

Commit 5fa6d3a

Browse files
committed
initial pass at conv layer viz
1 parent a7aba9e commit 5fa6d3a

File tree

4 files changed

+258
-2
lines changed

4 files changed

+258
-2
lines changed

kviz/conv.py

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"""
2+
Copyright 2021 Lance Galletti
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
18+
import numpy as np
19+
from PIL import Image as im
20+
import matplotlib.pyplot as plt
21+
from tensorflow.keras import models
22+
23+
24+
class ConvGraph():
25+
"""
26+
Class for creating and rendering visualization of Keras
27+
Sequential Model with Convolutional Layers
28+
29+
Attributes:
30+
model : tf.keras.Model
31+
a compiled keras sequential model
32+
33+
Methods:
34+
render :
35+
Shows all the convolution activations
36+
37+
"""
38+
39+
def __init__(self, model):
40+
self.model = model
41+
42+
43+
def _snap_layer(self, display_grid, scale, filename, xticks, yticks):
44+
fig, ax = plt.subplots(figsize=(int(scale * display_grid.shape[1]), int(scale * display_grid.shape[0])))
45+
ax.set_xticks(xticks)
46+
ax.set_yticks(yticks)
47+
ax.grid(True)
48+
ax.imshow(display_grid, aspect='auto')
49+
fig.savefig(filename + '.png', transparent=True)
50+
plt.close()
51+
return np.asarray(im.open(filename + '.png'))
52+
53+
54+
def animate(self, X=None, filename='conv_animation'):
55+
"""
56+
Render animation of a Convolutional layers based on a stream
57+
of input.
58+
59+
Parameters:
60+
X : ndarray
61+
input to a Keras model - ideally of the same class
62+
filename : str
63+
name of file to which visualization will be saved
64+
65+
Returns:
66+
None
67+
"""
68+
69+
layer_outputs = [layer.output for layer in self.model.layers]
70+
# Creates a model that will return these outputs, given the model input
71+
activation_model = models.Model(inputs=self.model.input, outputs=layer_outputs)
72+
images_per_row = 8
73+
74+
for i in range(len(self.model.layers)):
75+
# Ignore non-conv2d layers
76+
layer_name = self.model.layers[i].name
77+
if not layer_name.startswith("conv2d"):
78+
continue
79+
80+
images = []
81+
for j in range(len(X)):
82+
activations = activation_model.predict(X[j])
83+
# Number of features in the feature map
84+
n_features = activations[i].shape[-1]
85+
# The feature map has shape (1, size, size, n_features).
86+
size = activations[i].shape[1]
87+
# Tiles the activation channels in this matrix
88+
n_cols = n_features // images_per_row
89+
display_grid = np.zeros((size * n_cols, images_per_row * size))
90+
# Tiles each filter into a big horizontal grid
91+
for col in range(n_cols):
92+
for row in range(images_per_row):
93+
# Displays the grid
94+
display_grid[
95+
col * size: (col + 1) * size,
96+
row * size: (row + 1) * size] = activations[i][0, :, :, col * images_per_row + row]
97+
98+
images.append(
99+
im.fromarray(self._snap_layer(
100+
display_grid, 1. / size,
101+
filename + "_" + layer_name,
102+
xticks=np.linspace(0, display_grid.shape[1], images_per_row + 1),
103+
yticks=np.linspace(0, display_grid.shape[0], n_cols + 1))))
104+
105+
images[0].save(
106+
filename + "_" + layer_name + '.gif',
107+
optimize=False, # important for transparent background
108+
save_all=True,
109+
append_images=images[1:],
110+
loop=0,
111+
duration=100,
112+
transparency=255, # prevent PIL from making background black
113+
disposal=2
114+
)
115+
116+
return
117+
118+
119+
def render(self, X=None, filename='conv_filters'):
120+
"""
121+
Render visualization of a Convolutional keras model
122+
123+
Parameters:
124+
X : ndarray
125+
input to a Keras model
126+
filename : str
127+
name of file to which visualization will be saved
128+
129+
Returns:
130+
None
131+
"""
132+
133+
layer_outputs = [layer.output for layer in self.model.layers]
134+
# Creates a model that will return these outputs, given the model input
135+
activation_model = models.Model(inputs=self.model.input, outputs=layer_outputs)
136+
images_per_row = 8
137+
138+
for j in range(len(X)):
139+
activations = activation_model.predict(X[j])
140+
141+
for i in range(len(activations)):
142+
# Ignore non-conv2d layers
143+
layer_name = self.model.layers[i].name
144+
if not layer_name.startswith("conv2d"):
145+
continue
146+
147+
# Number of features in the feature map
148+
n_features = activations[i].shape[-1]
149+
# The feature map has shape (1, size, size, n_features).
150+
size = activations[i].shape[1]
151+
# Tiles the activation channels in this matrix
152+
n_cols = n_features // images_per_row
153+
display_grid = np.zeros((size * n_cols, images_per_row * size))
154+
# Tiles each filter into a big horizontal grid
155+
for col in range(n_cols):
156+
for row in range(images_per_row):
157+
# Displays the grid
158+
display_grid[
159+
col * size: (col + 1) * size,
160+
row * size: (row + 1) * size] = activations[i][0, :, :, col * images_per_row + row]
161+
162+
self._snap_layer(
163+
display_grid, 1. / size,
164+
filename + "_" + str(j) + "_" + layer_name,
165+
xticks=np.linspace(0, display_grid.shape[1], images_per_row + 1),
166+
yticks=np.linspace(0, display_grid.shape[0], n_cols + 1))
167+
168+
return

kviz/dense.py

+4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ class DenseGraph():
4949
is provided, show a GIF of the activations of each
5050
Neuron based on the input provided.
5151
52+
animate_learning:
53+
Make GIF from snapshots of decision boundary at
54+
given snap_freq
55+
5256
"""
5357

5458
def __init__(self, model):

tests/test_conv.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import numpy as np
2+
from tensorflow import keras
3+
from tensorflow.keras import layers, utils
4+
from tensorflow.keras.datasets import mnist
5+
6+
from kviz.conv import ConvGraph
7+
8+
9+
def test_conv_input():
10+
(X_train, y_train), (X_test, y_test) = mnist.load_data()
11+
12+
X_train = X_train.reshape(X_train.shape[0], 28, 28, 1)
13+
X_train = X_train.astype('float32')
14+
X_train /= 255
15+
16+
X_test = X_test.reshape(X_test.shape[0], 28, 28, 1)
17+
X_test = X_test.astype('float32')
18+
X_test /= 255
19+
20+
number_of_classes = 10
21+
Y_train = utils.to_categorical(y_train, number_of_classes)
22+
Y_test = utils.to_categorical(y_test, number_of_classes)
23+
24+
ACTIVATION = "relu"
25+
model = keras.models.Sequential()
26+
model.add(layers.Conv2D(32, 5, input_shape=(28, 28, 1), activation=ACTIVATION))
27+
model.add(layers.MaxPooling2D())
28+
model.add(layers.Conv2D(64, 5, activation=ACTIVATION))
29+
model.add(layers.MaxPooling2D())
30+
model.add(layers.Flatten())
31+
model.add(layers.Dense(100, activation=ACTIVATION))
32+
model.add(layers.Dense(10, activation="softmax"))
33+
model.compile(loss="categorical_crossentropy", metrics=['accuracy'])
34+
35+
model.fit(X_train, Y_train, batch_size=100, epochs=5)
36+
37+
score = model.evaluate(X_test, Y_test, verbose=0)
38+
print("Test loss:", score[0])
39+
print("Test accuracy:", score[1])
40+
41+
dg = ConvGraph(model)
42+
X = []
43+
for i in range(number_of_classes):
44+
X.append(np.expand_dims(X_train[np.where(y_train == i)[0][0]], axis=0))
45+
dg.render(X, filename='test_input_mnist')
46+
47+
48+
def test_conv_animate():
49+
(X_train, y_train), (X_test, y_test) = mnist.load_data()
50+
51+
X_train = X_train.reshape(X_train.shape[0], 28, 28, 1)
52+
X_train = X_train.astype('float32')
53+
X_train /= 255
54+
55+
X_test = X_test.reshape(X_test.shape[0], 28, 28, 1)
56+
X_test = X_test.astype('float32')
57+
X_test /= 255
58+
59+
number_of_classes = 10
60+
Y_train = utils.to_categorical(y_train, number_of_classes)
61+
Y_test = utils.to_categorical(y_test, number_of_classes)
62+
63+
ACTIVATION = "relu"
64+
model = keras.models.Sequential()
65+
model.add(layers.Conv2D(32, 5, input_shape=(28, 28, 1), activation=ACTIVATION))
66+
model.add(layers.MaxPooling2D())
67+
model.add(layers.Conv2D(64, 5, activation=ACTIVATION))
68+
model.add(layers.MaxPooling2D())
69+
model.add(layers.Flatten())
70+
model.add(layers.Dense(100, activation=ACTIVATION))
71+
model.add(layers.Dense(10, activation="softmax"))
72+
model.compile(loss="categorical_crossentropy", metrics=['accuracy'])
73+
74+
model.fit(X_train, Y_train, batch_size=100, epochs=5)
75+
76+
score = model.evaluate(X_test, Y_test, verbose=0)
77+
print("Test loss:", score[0])
78+
print("Test accuracy:", score[1])
79+
80+
dg = ConvGraph(model)
81+
X = []
82+
for i in range(min(50, len(np.where(y_train == 0)[0]))):
83+
X.append(np.expand_dims(X_train[np.where(y_train == 0)[0][i]], axis=0))
84+
dg.animate(X, filename='test_animate_mnist')

tests/test_viz.py tests/test_dense.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_dense_input_xor():
3535
[1, 1]])
3636
Y = np.array([x[0] ^ x[1] for x in X])
3737

38-
model.fit(X, Y, batch_size=4, epochs=1000)
38+
model.fit(X, Y, batch_size=4, epochs=100)
3939

4040
colors = np.array(['b', 'g'])
4141
fig, ax = plt.subplots()
@@ -71,7 +71,7 @@ def test_dense_input_line():
7171
X = np.array(t)
7272
Y = np.array([1 if x[0] - x[1] >= 0 else 0 for x in X])
7373

74-
model.fit(X, Y, batch_size=50, epochs=100)
74+
model.fit(X, Y, batch_size=50, epochs=10)
7575

7676
# see which nodes activate for a given class
7777
X0 = X[X[:, 0] - X[:, 1] <= 0]

0 commit comments

Comments
 (0)