|
a |
|
b/v3/py2tfjs/conversion_example/meshnet2tfjs.py |
|
|
1 |
import torch |
|
|
2 |
import numpy as np |
|
|
3 |
import json |
|
|
4 |
import os |
|
|
5 |
|
|
|
6 |
|
|
|
7 |
# Assuming `tfjs_spec` is your final dictionary containing the TensorFlow.js model specification |
|
|
8 |
# Let's save it to a file named 'model.json' |
|
|
9 |
def save_tfjs_model_spec(tfjs_spec, file_name="model.json"): |
|
|
10 |
with open(file_name, "w") as json_file: |
|
|
11 |
# Set `indent` to 4 for pretty-printing which makes the JSON file human-readable |
|
|
12 |
# Ensure `ensure_ascii` is False so that the file is saved in UTF-8 encoding |
|
|
13 |
json.dump(tfjs_spec, json_file, indent=4, ensure_ascii=False) |
|
|
14 |
|
|
|
15 |
|
|
|
16 |
def add_conv3d_layer( |
|
|
17 |
layers, filters, kernel_size, dilation, stride, layer_name |
|
|
18 |
): |
|
|
19 |
layers.append( |
|
|
20 |
{ |
|
|
21 |
"class_name": "Conv3D", |
|
|
22 |
"config": { |
|
|
23 |
"name": layer_name, |
|
|
24 |
"trainable": False, |
|
|
25 |
"filters": filters, |
|
|
26 |
"kernel_size": kernel_size, |
|
|
27 |
"strides": stride, |
|
|
28 |
"dilation_rate": dilation, |
|
|
29 |
"padding": "same", |
|
|
30 |
"data_format": "channels_last", |
|
|
31 |
"activation": "linear", # We will add the activation layer separately |
|
|
32 |
"use_bias": True, |
|
|
33 |
"dtype": "float32", |
|
|
34 |
}, |
|
|
35 |
"name": layer_name, |
|
|
36 |
"inbound_nodes": [[[layers[-1]["name"], 0, 0, {}]]], |
|
|
37 |
} |
|
|
38 |
) |
|
|
39 |
|
|
|
40 |
|
|
|
41 |
def add_activation_layer(layers, activation, layer_name): |
|
|
42 |
layers.append( |
|
|
43 |
{ |
|
|
44 |
"class_name": "Activation", |
|
|
45 |
"config": { |
|
|
46 |
"name": layer_name, |
|
|
47 |
"trainable": False, |
|
|
48 |
"dtype": "float32", |
|
|
49 |
"activation": activation, |
|
|
50 |
}, |
|
|
51 |
"name": layer_name, |
|
|
52 |
"inbound_nodes": [[[layers[-1]["name"], 0, 0, {}]]], |
|
|
53 |
} |
|
|
54 |
) |
|
|
55 |
|
|
|
56 |
|
|
|
57 |
def get_activation_name(pytorch_activation): |
|
|
58 |
tfjs_activations_map = { |
|
|
59 |
"ReLU": "relu", |
|
|
60 |
"ELU": "elu", |
|
|
61 |
"Sigmoid": "sigmoid", |
|
|
62 |
"Tanh": "tanh", |
|
|
63 |
"LeakyReLU": "leaky_relu", |
|
|
64 |
# Add more mappings as needed |
|
|
65 |
} |
|
|
66 |
|
|
|
67 |
class_name = pytorch_activation.__class__.__name__ |
|
|
68 |
if class_name in tfjs_activations_map: |
|
|
69 |
return tfjs_activations_map[class_name] |
|
|
70 |
else: |
|
|
71 |
raise ValueError(f"Unsupported PyTorch activation type: {class_name}") |
|
|
72 |
|
|
|
73 |
|
|
|
74 |
def convert_pytorch_to_tfjs(pytorch_model, cube_size): |
|
|
75 |
layers = [ |
|
|
76 |
{ |
|
|
77 |
"class_name": "InputLayer", |
|
|
78 |
"config": { |
|
|
79 |
"batch_input_shape": [None, cube_size, cube_size, cube_size, 1], |
|
|
80 |
"dtype": "float32", |
|
|
81 |
"sparse": False, |
|
|
82 |
"ragged": False, |
|
|
83 |
"name": "input", |
|
|
84 |
}, |
|
|
85 |
"name": "input", |
|
|
86 |
"inbound_nodes": [], |
|
|
87 |
} |
|
|
88 |
] |
|
|
89 |
|
|
|
90 |
layer_count = 0 # Counter for naming layers |
|
|
91 |
|
|
|
92 |
# Process each PyTorch layer and add corresponding layers to the config |
|
|
93 |
for i, module in enumerate(pytorch_model.model): |
|
|
94 |
if isinstance(module, torch.nn.Sequential): |
|
|
95 |
for layer in module: |
|
|
96 |
if isinstance(layer, torch.nn.Conv3d): |
|
|
97 |
layer_name = f"conv3d_{layer_count}" |
|
|
98 |
add_conv3d_layer( |
|
|
99 |
layers, |
|
|
100 |
layer.out_channels, |
|
|
101 |
list(layer.kernel_size), |
|
|
102 |
list(layer.dilation), |
|
|
103 |
list(layer.stride), |
|
|
104 |
layer_name, |
|
|
105 |
) |
|
|
106 |
else: |
|
|
107 |
activation_name = f"activation_{layer_count}" |
|
|
108 |
add_activation_layer( |
|
|
109 |
layers, get_activation_name(layer), activation_name |
|
|
110 |
) |
|
|
111 |
layer_count += 1 |
|
|
112 |
elif isinstance(module, torch.nn.Conv3d): |
|
|
113 |
layer_name = "output" |
|
|
114 |
add_conv3d_layer( |
|
|
115 |
layers, |
|
|
116 |
module.out_channels, |
|
|
117 |
list(module.kernel_size), |
|
|
118 |
list(module.dilation), |
|
|
119 |
list(module.stride), |
|
|
120 |
layer_name, |
|
|
121 |
) |
|
|
122 |
layer_count += 1 |
|
|
123 |
|
|
|
124 |
# Name the last layer "output" |
|
|
125 |
layers[-1]["name"] = "output" |
|
|
126 |
|
|
|
127 |
# Generate the output model specification for TensorFlow.js |
|
|
128 |
tfjs_spec = { |
|
|
129 |
"format": "layers-model", |
|
|
130 |
"generatedBy": "keras v2.7.0", |
|
|
131 |
"convertedBy": "TensorFlow.js Converter v3.9.0", |
|
|
132 |
"modelTopology": { |
|
|
133 |
"keras_version": "2.6.0", |
|
|
134 |
"backend": "tensorflow", |
|
|
135 |
"model_config": { |
|
|
136 |
"class_name": "Functional", |
|
|
137 |
"config": { |
|
|
138 |
"name": "model", |
|
|
139 |
"layers": layers, |
|
|
140 |
"input_layers": [["input", 0, 0]], |
|
|
141 |
"output_layers": [[layers[-1]["name"], 0, 0]], |
|
|
142 |
}, |
|
|
143 |
}, |
|
|
144 |
}, |
|
|
145 |
} |
|
|
146 |
|
|
|
147 |
# Initialize the weights manifest list |
|
|
148 |
weights_manifest = [] |
|
|
149 |
|
|
|
150 |
input_channels = 1 |
|
|
151 |
# Iterate through each layer in the layers list |
|
|
152 |
for layer in layers: |
|
|
153 |
class_name = layer["class_name"] |
|
|
154 |
if class_name == "Conv3D": |
|
|
155 |
# For Conv3D layers, we have 'kernel' (weights) and 'bias' |
|
|
156 |
kernel_name = f"{layer['name']}/kernel" |
|
|
157 |
bias_name = f"{layer['name']}/bias" |
|
|
158 |
|
|
|
159 |
# Get the shape of the kernel and bias |
|
|
160 |
config = layer["config"] |
|
|
161 |
kernel_shape = [ |
|
|
162 |
config["kernel_size"][0], |
|
|
163 |
config["kernel_size"][1], |
|
|
164 |
config["kernel_size"][2], |
|
|
165 |
input_channels, |
|
|
166 |
config["filters"], |
|
|
167 |
] |
|
|
168 |
bias_shape = [config["filters"]] |
|
|
169 |
cube_size = config["filters"] # Update cube size for the next layer |
|
|
170 |
|
|
|
171 |
# Append the weights and bias configurations to the weights manifest |
|
|
172 |
weights_manifest.extend( |
|
|
173 |
[ |
|
|
174 |
{ |
|
|
175 |
"name": kernel_name, |
|
|
176 |
"shape": kernel_shape, |
|
|
177 |
"dtype": "float32", |
|
|
178 |
}, |
|
|
179 |
{ |
|
|
180 |
"name": bias_name, |
|
|
181 |
"shape": bias_shape, |
|
|
182 |
"dtype": "float32", |
|
|
183 |
}, |
|
|
184 |
] |
|
|
185 |
) |
|
|
186 |
# set input channels for the next layer |
|
|
187 |
input_channels = config["filters"] |
|
|
188 |
|
|
|
189 |
# Add the weightsManifest to the tfjs_spec |
|
|
190 |
tfjs_spec["weightsManifest"] = [ |
|
|
191 |
{"paths": ["model.bin"], "weights": weights_manifest} |
|
|
192 |
] |
|
|
193 |
|
|
|
194 |
return tfjs_spec |
|
|
195 |
|
|
|
196 |
|
|
|
197 |
def extract_weights(layer): |
|
|
198 |
# Extract the weights and biases, move to CPU and convert to numpy arrays |
|
|
199 |
layer_weight = layer.weight.data.cpu().numpy() |
|
|
200 |
layer_bias = layer.bias.data.cpu().numpy() |
|
|
201 |
# Transpose the weights to match TensorFlow.js's expected order |
|
|
202 |
# From: (out_channels, in_channels, depth, height, width) |
|
|
203 |
# To: (height, width, depth, in_channels, out_channels) |
|
|
204 |
layer_weight_tfjs = np.transpose(layer_weight, (2, 3, 4, 1, 0)).flatten() |
|
|
205 |
|
|
|
206 |
# Flatten the biases |
|
|
207 |
layer_bias_tfjs = layer_bias.flatten() |
|
|
208 |
|
|
|
209 |
return layer_weight_tfjs, layer_bias_tfjs |
|
|
210 |
|
|
|
211 |
|
|
|
212 |
def save_pytorch_weights_to_bin(pytorch_model, bin_file_path="model.bin"): |
|
|
213 |
# This will hold all weights and biases in the correct order |
|
|
214 |
all_weights = [] |
|
|
215 |
|
|
|
216 |
# Loop through each model layer |
|
|
217 |
for layer in pytorch_model.model: |
|
|
218 |
# If the layer is a Sequential, recursively process its layers |
|
|
219 |
if isinstance(layer, torch.nn.Sequential): |
|
|
220 |
for sublayer in layer: |
|
|
221 |
if isinstance(sublayer, torch.nn.Conv3d): |
|
|
222 |
weights, biases = extract_weights(sublayer) |
|
|
223 |
all_weights.extend([weights, biases]) |
|
|
224 |
elif isinstance(layer, torch.nn.Conv3d): |
|
|
225 |
weights, biases = extract_weights(layer) |
|
|
226 |
all_weights.extend([weights, biases]) |
|
|
227 |
|
|
|
228 |
# Concatenate all the flattened weights and biases |
|
|
229 |
combined_weights = np.concatenate(all_weights) |
|
|
230 |
|
|
|
231 |
# Convert the concatenated array to bytes |
|
|
232 |
weights_bytes = combined_weights.tobytes() |
|
|
233 |
|
|
|
234 |
# Write the bytes to a .bin file |
|
|
235 |
with open(bin_file_path, "wb") as bin_file: |
|
|
236 |
bin_file.write(weights_bytes) |
|
|
237 |
|
|
|
238 |
|
|
|
239 |
def meshnet2tfjs(model, dirname): |
|
|
240 |
# Ensure the directory exists |
|
|
241 |
if not os.path.exists(dirname): |
|
|
242 |
os.makedirs(dirname) |
|
|
243 |
|
|
|
244 |
# Convert the PyTorch model to TensorFlow.js model specification |
|
|
245 |
tfjs_model_spec = convert_pytorch_to_tfjs(model, 256) |
|
|
246 |
|
|
|
247 |
# Save the TensorFlow.js model specification JSON file |
|
|
248 |
model_json_path = os.path.join(dirname, "model.json") |
|
|
249 |
save_tfjs_model_spec(tfjs_model_spec, model_json_path) |
|
|
250 |
|
|
|
251 |
# Save the PyTorch model weights to a binary file |
|
|
252 |
model_bin_path = os.path.join(dirname, "model.bin") |
|
|
253 |
save_pytorch_weights_to_bin(model, model_bin_path) |
|
|
254 |
|
|
|
255 |
|
|
|
256 |
# meshnet2tfjs(model, "/tmp/testmodel") |