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")