Skip to content

Commit d05efc8

Browse files
Add files via upload
0 parents  commit d05efc8

File tree

99 files changed

+2466
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+2466
-0
lines changed

Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
FROM dolphonie1/causal_skipper:0.01
2+
3+
# add custom shap lib. This dockerfile should be built from the root of this repo
4+
# with the shap dir inside
5+
WORKDIR /src
6+
RUN mkdir /src/drone_causality && mkdir /src/shap
7+
COPY ./shap /src/shap
8+
RUN pip install /src/shap
9+
10+
# install opencv deps for shap
11+
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && apt-get install ffmpeg libsm6 libxext6 -y
12+
13+
RUN pip install seaborn
14+
15+
# add current repo contents
16+
COPY . /src/drone_causality/

analysis/analyze_study.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import argparse
2+
3+
import joblib
4+
import optuna
5+
from optuna import Study
6+
from optuna.visualization import plot_optimization_history, plot_intermediate_values, plot_parallel_coordinate, \
7+
plot_contour, plot_param_importances, plot_slice
8+
9+
10+
def analyze_study(study_name: str, storage_name: str):
11+
study = optuna.create_study(study_name=study_name, storage=storage_name, load_if_exists=True,
12+
direction="minimize")
13+
print(study.best_trial.params)
14+
graph_study(study)
15+
16+
17+
def analyze_local(file_path: str):
18+
study = joblib.load(file_path)
19+
print(study.best_trial.params)
20+
graph_study(study)
21+
22+
23+
def graph_study(study: Study):
24+
fig = plot_optimization_history(study)
25+
fig.show()
26+
fig2 = plot_intermediate_values(study)
27+
fig2.show()
28+
fig3 = plot_parallel_coordinate(study)
29+
fig3.show()
30+
fig4 = plot_contour(study)
31+
fig4.show()
32+
fig5 = plot_param_importances(study)
33+
fig5.show()
34+
fig6 = plot_slice(study)
35+
fig6.show()
36+
best_params = study.best_params
37+
print(best_params)
38+
print(study.best_value)
39+
40+
41+
if __name__ == "__main__":
42+
parser = argparse.ArgumentParser()
43+
parser.add_argument("storage_path", type=str)
44+
parser.add_argument("--study_name", type=str, default=None)
45+
args = parser.parse_args()
46+
if args.study_name is not None:
47+
analyze_study(args.study_name, args.storage_path)
48+
else:
49+
analyze_local(args.storage_path)

analysis/calculate_output_noise.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Created by Patrick Kao at 4/4/22
2+
import argparse
3+
4+
import numpy as np
5+
from PIL import Image
6+
from tensorflow import keras
7+
8+
from utils.vis_utils import parse_params_json
9+
from utils.model_utils import load_model_from_weights, get_readable_name, generate_hidden_list
10+
11+
12+
def calculate_output_noise(params_path: str, input_img_path: str, noise: float, n_trials=50):
13+
input_img = np.expand_dims(np.array(Image.open(input_img_path), dtype=float), axis=0)
14+
model_variances = {}
15+
var_layer = keras.layers.GaussianNoise(stddev=noise)
16+
for local_path, model_path, model_params in parse_params_json(params_path):
17+
model = load_model_from_weights(model_params, model_path)
18+
imgs = []
19+
for _ in range(n_trials):
20+
# might want to try with nonzero hiddens at some point
21+
hiddens = generate_hidden_list(model=model, return_numpy=True)
22+
noise_img = var_layer(input_img, training=True)
23+
output = model.predict([noise_img, *hiddens])
24+
imgs.append(output[0]) # don't save hidden output
25+
26+
channel_variances = np.var(imgs, axis=0)
27+
avg_variance = np.mean(channel_variances)
28+
model_variances[get_readable_name(model_params)] = avg_variance
29+
30+
return model_variances
31+
32+
33+
if __name__ == "__main__":
34+
parser = argparse.ArgumentParser()
35+
parser.add_argument("params_path")
36+
parser.add_argument("input_img")
37+
parser.add_argument("noise", type=float)
38+
args = parser.parse_args()
39+
print(calculate_output_noise(args.params_path, args.input_img, args.noise))

analysis/grad_cam.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Created by Patrick Kao at 3/9/22
2+
from math import ceil
3+
from typing import Optional, Sequence, Union
4+
5+
import tensorflow as tf
6+
from numpy import ndarray
7+
from tensorflow import Tensor
8+
from tensorflow.python.keras import Model
9+
from tensorflow.python.keras.layers import Conv2D
10+
from tensorflow.python.keras.models import Functional
11+
12+
from utils.vis_utils import image_grid
13+
from utils.model_utils import load_model_from_weights, load_model_no_params, ModelParams
14+
15+
16+
def compute_gradcam(img: Union[Tensor, ndarray], grad_model: Functional, hiddens: Sequence[Tensor],
17+
pred_index: Optional[Sequence[Tensor]] = None):
18+
heatmaps, hiddens = _compute_gradcam(img=img, grad_model=grad_model, hiddens=hiddens, pred_index=pred_index)
19+
avg_heat = tf.math.add_n(heatmaps)
20+
avg_heat = tf.expand_dims(avg_heat, axis=-1)
21+
return avg_heat, hiddens
22+
23+
24+
def compute_gradcam_tile(img: Union[Tensor, ndarray], grad_model: Functional, hiddens: Sequence[Tensor],
25+
pred_index: Optional[Sequence[Tensor]] = None):
26+
heatmaps, hiddens = _compute_gradcam(img=img, grad_model=grad_model, hiddens=hiddens, pred_index=pred_index)
27+
num_rows = ceil(len(heatmaps) / 2)
28+
return image_grid(imgs=heatmaps, rows=num_rows, cols=2), hiddens
29+
30+
31+
def _compute_gradcam(img: Union[Tensor, ndarray], grad_model: Functional, hiddens: Sequence[Tensor],
32+
pred_index: Optional[Sequence[Tensor]] = None):
33+
"""
34+
Adaptation of grad-cam code at https://keras.io/examples/vision/grad_cam/ with
35+
the following adjustments:
36+
37+
- because we want the impact of pixels not on a class decision but on any of the 4 axes, sum heatmaps for all 4 outputs
38+
- because we don't care about positive or negative impact, drop the ReLU (wasn't in this implementation to begin with)
39+
- Before adding heatmaps together, take absolute value of each heatmap because don't care if positive or negative contribution to direction
40+
- idea (not implemented): instead of just upscaling, multiply grad-cam heatmap against visual backprop heatmap to model actual network weights
41+
:return tuple of image tensor (height, width, 1) with shape of final conv layer, list of hidden vectors each of shape
42+
(batch, hidden_dim)
43+
"""
44+
if pred_index is None:
45+
pred_index = range(grad_model.output_shape[1][-1])
46+
47+
# Then, we compute the gradient of the top predicted class for our input image
48+
# with respect to the activations of the last conv layer
49+
with tf.GradientTape() as tape:
50+
out = grad_model([img, *hiddens])
51+
last_conv_layer_output = out[0]
52+
preds = out[1]
53+
hiddens = out[2:]
54+
55+
heatmaps = []
56+
# for each element of preds, compute gradient of last_conv_out wrt this element of pred, abs and sum these gradients
57+
# strip batch dim
58+
# jacobian shape 4x last_conv_layer_output.shape where each element is gradient, preds[:,i] wrt last_conv_layer_out
59+
grads = tape.jacobian(preds, last_conv_layer_output)[0]
60+
last_conv_layer_output = last_conv_layer_output[0]
61+
for pred in pred_index:
62+
# This is the gradient of the output neuron (top pred1icted or chosen)
63+
# with regard to the output feature map of the last conv layer
64+
grad = grads[pred]
65+
66+
# This is a vector where each entry is the mean intensity of the gradient
67+
# over a specific feature map channel
68+
pooled_grads = tf.reduce_mean(grad, axis=(0, 1, 2))
69+
70+
# We multiply each channel in the feature map array
71+
# by "how important this channel is" with regard to the top predicted class
72+
# then sum all the channels to obtain the heatmap class activation
73+
heatmap = last_conv_layer_output @ pooled_grads[..., tf.newaxis]
74+
heatmap = tf.squeeze(heatmap)
75+
76+
# patrick edit: absolute value heatmaps to not discount/cancel negative and positive contributions
77+
heatmap = tf.math.abs(heatmap)
78+
79+
heatmaps.append(heatmap)
80+
81+
return heatmaps, hiddens
82+
83+
84+
def get_last_conv(model_path: str, model_params: Optional[ModelParams] = None) -> Model:
85+
if model_params is not None:
86+
model_params.single_step = True
87+
vis_model = load_model_from_weights(model_params, checkpoint_path=model_path)
88+
else:
89+
vis_model = load_model_no_params(model_path, single_step=True)
90+
91+
# get last conv layer
92+
# cleave off only convolutional head
93+
conv_layers = [layer for layer in vis_model.layers if isinstance(layer, Conv2D)]
94+
95+
# First, we create a model that maps the input image to the activations
96+
# of the last conv layer as well as the output predictions
97+
return tf.keras.models.Model(
98+
[vis_model.inputs], [conv_layers[-1].output, *vis_model.output]
99+
)

analysis/input_grad.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Created by Patrick Kao at 3/10/22
2+
from typing import Sequence, Union
3+
4+
import tensorflow as tf
5+
from numpy import ndarray
6+
from tensorflow import Tensor
7+
from tensorflow.python.keras.models import Functional
8+
9+
10+
def compute_input_grad(img: Union[Tensor, ndarray], model: Functional, hiddens: Sequence[Tensor]):
11+
"""
12+
Computes gradients of model output with respect to img
13+
:param img:
14+
:param model:
15+
:param hiddens:
16+
:return: tuple of image tensor (height, width, 1) with shape of input img, list of hidden vectors each of shape
17+
(batch, hidden_dim)
18+
"""
19+
with tf.GradientTape() as tape:
20+
tape.watch(img)
21+
out = model([img, *hiddens])
22+
preds = out[0]
23+
hiddens = out[1:]
24+
25+
grads = tape.jacobian(preds, img)[0] # shape: 4 x 1 x height x width x channels
26+
grads = tf.math.abs(grads) # take absolute value so + and - impacts don't cancel each other out
27+
28+
heatmap = tf.math.reduce_sum(grads, axis=0) # shape 1 x height x width x channels
29+
heatmap = tf.squeeze(heatmap, axis=0)
30+
# convert heatmap to black and white by summing channels
31+
heatmap = tf.math.reduce_sum(heatmap, axis=-1, keepdims=True)
32+
return heatmap, hiddens, None

analysis/lipschitz_constant.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Created by Patrick Kao at 4/4/22
2+
import argparse
3+
import json
4+
import os
5+
from pathlib import Path
6+
from typing import Sequence, Optional, Dict, Tuple
7+
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
from numpy import ndarray
11+
from tqdm import tqdm
12+
13+
from keras_models import IMAGE_SHAPE
14+
from utils.data_utils import image_dir_generator
15+
from utils.model_utils import ModelParams, load_model_from_weights, generate_hidden_list, get_readable_name
16+
from utils.vis_utils import parse_params_json
17+
18+
19+
def calculate_lipschitz_constant(model_path: str, model_params: ModelParams, sequence_path: str,
20+
reverse_channels: bool) -> Sequence[ndarray]:
21+
model = load_model_from_weights(model_params, model_path)
22+
hiddens = generate_hidden_list(model=model, return_numpy=True)
23+
all_hiddens = [] # list of list of arrays with shape num_timesteps x num_hiddens x hidden_dim
24+
for i, img in tqdm(enumerate(image_dir_generator(sequence_path, IMAGE_SHAPE, reverse_channels))):
25+
all_hiddens.append(hiddens)
26+
out = model.predict([img, *hiddens])
27+
hiddens = out[1:] # list num_hidden long, each el is hidden_dim,
28+
29+
# flatten batch dim
30+
all_hiddens = [[np.squeeze(hid, axis=0) for hid in step_hid] for step_hid in all_hiddens]
31+
# crete list with same shape as hidden vectors where contents are lipschitz values of each dimension
32+
lip = [np.zeros_like(h) for h in all_hiddens[0]]
33+
for i in range(len(all_hiddens) - 1):
34+
current_hiddens = all_hiddens[i]
35+
next_hiddens = all_hiddens[i + 1]
36+
diff = [np.abs(n - c) for n, c in zip(next_hiddens, current_hiddens)]
37+
lip = [np.maximum(l, d) for l, d in zip(lip, diff)]
38+
return lip
39+
40+
41+
def graph_lipschitz_constant(lip_mean: Dict[str, ndarray], lip_std: Optional[Dict[str, ndarray]],
42+
display_result: bool = False,
43+
save_path: Optional[str] = None):
44+
plt.clf()
45+
plt.xlabel("Node Rank")
46+
plt.ylabel("Lipschitz Constant of Hidden State Nodes")
47+
48+
# concat all hidden dims into 1d
49+
for model_name, hiddens in lip_mean.items():
50+
# hiddens shape: flattened_hidden_dim
51+
sort_order = np.argsort(hiddens)
52+
53+
lip_sorted = hiddens[sort_order]
54+
lip_x = np.linspace(0, 1, num=len(lip_sorted))
55+
plt.plot(lip_x, lip_sorted, label=model_name)
56+
if lip_std is not None:
57+
std = lip_std[model_name]
58+
std_sorted = std[sort_order]
59+
plt.fill_between(lip_x, lip_sorted + std_sorted, lip_sorted - std_sorted, alpha=0.5)
60+
61+
plt.legend(loc="upper left")
62+
# note that save needs to happen before plt.show() b/c plt.show() clears figures
63+
if save_path is not None:
64+
plt.savefig(save_path)
65+
66+
if display_result:
67+
plt.show()
68+
69+
70+
def params_lipschitz_constant(datasets_json: str, params_path: str, display_result: bool = False,
71+
save_dir: Optional[str] = None) -> Tuple[Dict, Dict]:
72+
with open(datasets_json, "r") as f:
73+
datasets: Dict[str, Tuple[str, bool]] = json.load(f)
74+
75+
all_mean = {}
76+
all_std = {}
77+
if save_dir:
78+
Path(save_dir).mkdir(parents=True, exist_ok=True)
79+
for local_path, model_path, model_params in parse_params_json(params_path):
80+
model_name = get_readable_name(model_params)
81+
if model_name == "tcn":
82+
# TCN has no hidden state
83+
continue
84+
85+
lips = []
86+
for dataset_name, (data_path, reverse_channels, csv_path) in datasets.items():
87+
lip = calculate_lipschitz_constant(model_path, model_params, data_path, reverse_channels=reverse_channels)
88+
lips.append(lip)
89+
90+
# lips shape: num_datasets x num_hiddens x hidden dim
91+
# shape : num_dataset x flattened_hidden_dim
92+
lips_flat = np.array([np.hstack(dataset) for dataset in lips])
93+
lip_mean = np.mean(lips_flat, axis=0)
94+
lip_std = np.std(lips_flat, axis=0)
95+
model_lip_mean = {model_name: lip_mean}
96+
model_lip_std = {model_name: lip_std}
97+
if display_result or save_dir is not None:
98+
save_path = os.path.join(save_dir, model_name) if save_dir is not None else None
99+
graph_lipschitz_constant(lip_mean=model_lip_mean, lip_std=model_lip_std, display_result=display_result,
100+
save_path=save_path)
101+
102+
all_mean.update(model_lip_mean)
103+
all_std.update(model_lip_std)
104+
105+
# graph all lipschitz
106+
if display_result or save_dir is not None:
107+
graph_lipschitz_constant(lip_mean=all_mean, lip_std=all_std, display_result=display_result,
108+
save_path=os.path.join(save_dir, "all_lipschitz"))
109+
110+
if save_dir is not None:
111+
with open(os.path.join(save_dir, "lip_data.json"), "w") as f:
112+
lip_data = {"means": convert_values_to_list(all_mean), "stds": convert_values_to_list(all_std)}
113+
json.dump(lip_data, f)
114+
115+
return all_mean, all_std
116+
117+
118+
def convert_values_to_list(to_convert: Dict[str, ndarray]):
119+
to_ret = {}
120+
for key, np_arr in to_convert.items():
121+
to_ret[key] = list(np_arr)
122+
123+
return to_ret
124+
125+
126+
if __name__ == "__main__":
127+
parser = argparse.ArgumentParser()
128+
parser.add_argument("datasets_json")
129+
parser.add_argument("params_path")
130+
parser.add_argument("--display_result", action="store_true")
131+
parser.add_argument("--save_dir", type=str, default="lipschitz_out")
132+
args = parser.parse_args()
133+
params_lipschitz_constant(datasets_json=args.datasets_json, params_path=args.params_path,
134+
display_result=args.display_result, save_dir=args.save_dir)

0 commit comments

Comments
 (0)