使用MindConverter迁移模型定义脚本

Linux Ascend 模型开发 初级

工具概述

MindConverter是一款模型迁移工具,可将PyTorch(ONNX)或Tensorflow(PB)模型快速迁移到MindSpore框架下使用。模型文件(ONNX/PB)包含网络模型结构(network)与权重信息(weights),迁移后将生成MindSpore框架下的模型定义脚本(model.py)与权重文件(ckpt)。

mindconverter-overview

快速开始

MindConverter属于MindInsight的子模块,安装MindInsight后,即可使用MindConverter,MindInsight安装请参考安装文档。MindConverter命令行如下,更多CLI参数请参考命令行参数说明

mindconverter --model_file /path/to/model_file --shape SHAPE --input_nodes INPUTS --output_nodes OUTPUTS
  • --model_file指定模型文件路径,模型文件支持onnxpb格式。

  • --shape指定模型输入shape信息,多输入场景以空格分隔。

  • --input_nodes指定模型输入节点名称,多输入场景以空格分隔。

  • --output_nodes指定模型输出节点名称,多输出场景以空格分隔。

  • 转换结果默认输出到$PWD/output

环境依赖

使用MindConverter前需要安装以下依赖包,建议在x86环境下安装。ARM环境请参考常见问题

# 安装配套版本的MindSpore(以r1.2版本为例)
pip install mindspore~=1.2.0

# 安装onnx相关的依赖包
pip install onnx~=1.8.0
pip install onnxoptimizer~=0.1.2
pip install onnxruntime~=1.5.2

# 如果使用 Tensorflow PB 文件转换,则需安装tf2onnx
pip install tf2onnx~=1.7.1

迁移方案

一个网络模型工程,通常包含以下四个主要组成部分,各部分的迁移指引如下:

  • 模型定义(model.py

    1. 使用MindConverter工具转换模型结构。

    2. 手工调整可读性(可选)。

    3. 转换后的模型内嵌到原框架工程,验证转换等价性,参考常见问题

  • 数据处理(dataset.py

    1. 内置数据集可查询接口映射辅助转换。

    2. 自定义数据集与相关数据处理,可参考转换模板

  • 模型训练(train.py

    1. 损失函数(loss_fn),可查询接口映射或自定义实现。

    2. 优化器(optimizer),可查询接口映射或自定义实现。

    3. 模型训练的代码比较灵活,代码组织风格与MindSpore图模式差异较大,建议自行实现,参考转换模板

  • 模型推理(eval.py

    1. 度量指标(metric),可查询接口映射或自定义实现。

    2. 模型推理的代码比较灵活,代码组织风格与MindSpore图模式差异较大,建议自行实现,参考转换模板

实践步骤

第0步:导出模型文件

以PyTorch框架为例导出ONNX模型文件(Tensorflow框架请参考常见问题),需要Pytorch算子支持相应的ONNX算子,详情参考PytorchONNX的算子列表,操作步骤如下:

  1. 下载网络模型工程的源码、权重文件、数据集。

  2. 分析模型定义代码,整改forward函数的入参列表,确保入参均为Tensor类型,参考常见问题

  3. 从模型推理的代码中,识别模型对象(model)与输入的shape信息,导出ONNX文件。

    import torch
    from project.model import Model as PyTorchModel
    
    model = PyTorchModel()
    input_shape = (1, 3, 224, 224)
    input_tensor = torch.randn(*input_shape)
    torch.onnx.export(model, input_tensor, '/path/to/model.onnx')
    
  4. 验证ONNX模型与原脚本精度是否一致。

    import onnxruntime
    import numpy as np
    
    session = onnxruntime.InferenceSession('/path/to/model.onnx')
    input_node = session.get_inputs()[0]
    output = session.run(None, {input_node.name: np.load('/path/to/input.npy')})
    np.allclose(output, np.load('/path/to/output.npy'))
    

第1步:转换模型定义

执行MindConverter CLI命令,生成MindSpore模型文件(model.py)、权重信息(ckpt)、转换报告与权重映射表

mindconverter --model_file /path/to/model.onnx \
              --shape 1,3,224,224 \
              --input_nodes input_node_name \
              --output_nodes output_node_name

使用ONNX模型文件迁移,需要先从.onnx文件中获取模型输入节点shape、输入节点名称、输出节点名称,推荐使用Netron工具加载ONNX模型文件,获取上述信息。

模型文件(model.py)与权重信息(ckpt)可用于验证模型迁移的等价性,也可用于导出MindIR格式文件。

import mindspore
import numpy as np
from project.model import Network as MindSporeNetwork

network = MindSporeNetwork()
param_dict = mindspore.load_checkpoint('network.ckpt')
mindspore.load_param_into_net(network, param_dict)

input_data = mindspore.Tensor(np.load('/path/to/input.npy'))
output_benchmark = mindspore.Tensor(np.load('/path/to/output.npy'))

# 验证迁移等价性
output_data = network(input_data)
np.allclose(output_data, output_benchmark)

# 导出MindIR文件
mindspore.export(network, input_data, file_name='network_name', file_format='MINDIR')

注意事项:

  1. 由于模型转换工具以推理模式加载ONNX文件,转换后会导致网络中Dropout算子丢失,需要用户手动补齐。

  2. 模型转换工具本质上为算子驱动,对于MindConverter未维护的ONNX算子与MindSpore算子映射,将会出现相应的算子无法转换的问题,对于该类算子,用户可手动修改,或基于MindConverter实现映射关系,向MindInsight仓库贡献

  3. 在使用基于计算图的迁移时,MindConverter会根据--shape参数将模型输入的批次大小(batch size)、句子长度(sequence length)、图片尺寸(image shape)等尺寸相关参数固定下来,用户需要保证基于MindSpore重训练、推理时输入shape与转换时一致;若需要调整输入尺寸,请重新指定--shape进行转换,或修改转换后脚本中涉及张量尺寸变更操作相应的操作数。

  4. 脚本文件和权重文件输出于同一个目录下,转换报告和权重映射表输出于同一个目录下。

  5. 模型文件的安全性与一致性请用户自行保证。

第2步:转换数据处理

内置数据集可直接查询接口映射,自定义数据集需要自行实现,更多转换方案可参考编程指南

PyTorch源码如下:

from torch.utils.data import Dataset, DataLoader
from torchvision import transforms

class CustomDataset(Dataset):
    def __init__(self, *args, **kwargs):
        self.records = [...]
        self.labels = [...]
        # 定义数据增强
        self.transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)),
        ])
    def __len__(self):
        return len(self.labels)
    def __getitem__(self, idx):
        # 执行数据增强
        data = self.transform(self.records[idx])
        return data, self.labels[idx]

dataset = CustomDataset(*args, **kwargs)
data_loader = DataLoader(dataset, batch_size=BATCH_SIZE)

对应MindSpore代码如下:

from mindspore.dataset import GeneratorDataset
from mindspore.dataset.vision import py_transforms as transforms

class CustomGenerator:
    def __init__(self, *args, **kwargs):
        self.records = [...]
        self.labels = [...]
        # 定义数据增强
        self.transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)),
        ])
    def __len__(self):
        return len(self.labels)
    def __getitem__(self, idx):
        # 执行数据增强
        data = self.transform(self.records[idx])
        return data, self.labels[idx]

generator = CustomGenerator(*args, **kwargs)
dataset = GeneratorDataset(generator, column_names=['data', 'label']).batch(BATCH_SIZE)

第3步:转换模型训练

损失函数(loss_fn)可查询接口映射或自定义实现,更多转换方案可参考编程指南

优化器(optimizer)可查询接口映射或自定义实现,更多转换方案可参考编程指南

模型训练的代码比较灵活,代码组织风格与MindSpore图模式差异较大,建议自行实现,更多转换方案可参考编程指南

PyTorch源码如下:

import torch
from project.model import Network as PyTorchNetwork

# 创建网络模型实例
network = PyTorchNetwork()

# 定义优化器与学习率
optimizer = torch.optim.SGD(network.parameters(), lr=LEARNING_RATE)
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer)

# 执行模型训练
for i in range(EPOCH_SIZE):
    for data, label in data_loader:
        optimizer.zero_grad()
        output = network(data)
        loss = loss_fn(output, label)
        loss.backward()
        optimizer.step()
    scheduler.step()

对应MindSpore代码(Low-Level API)如下:

from mindspore import nn
from project.model import Network as MindSporeNetwork

# 创建网络模型实例
network = MindSporeNetwork()

# 定义学习率与优化器
scheduler = nn.ExponentialDecayLR(LEARNING_RATE)
optimizer = nn.SGD(params=network.trainable_params(), learning_rate=scheduler)

# 执行模型训练
net_with_loss = nn.WithLossCell(network, loss_fn)
train_network = nn.TrainOneStepCell(net_with_loss, optimizer)
train_network.set_train()
data_iterator = dataset.create_tuple_iterator(num_epochs=EPOCH_SIZE)
for i in range(EPOCH_SIZE):
    for data, label in data_iterator:
        loss = train_network(data, label)

对应MindSpore代码(High-Level API)如下:

from mindspore import nn
from mindspore import Model
from project.model import Network as MindSporeNetwork

# 创建网络模型实例
network = MindSporeNetwork()

# 定义学习率与优化器
scheduler = nn.ExponentialDecayLR(LEARNING_RATE)
optimizer = nn.SGD(params=network.trainable_params(), learning_rate=scheduler)

# 执行模型训练
model = Model(network, loss_fn=loss_fn, optimizer=optimizer)
model.train(EPOCH_SIZE, dataset)

第4步:转换模型推理

度量指标(metric),可查询接口映射或自定义实现。

模型推理的代码比较灵活,代码组织风格与MindSpore图模式差异较大,建议自行实现,更多转换方案可参考编程指南

PyTorch源码如下:

from project.model import Network as PyTorchNetwork

network = PyTorchNetwork()
for data, label in data_loader:
    output = network(data)
    loss = loss_fn(output, label)

对应MindSpore代码(Low-Level API)如下:

from mindspore import Model
from project.model import Network as MindSporeNetwork

network = MindSporeNetwork()
data_iterator = dataset.create_tuple_iterator()
for data, label in data_iterator:
    output = network(data)
    loss = loss_fn(output, label)

对应MindSpore代码(High-Level API)如下:

from mindspore import Model
from project.model import Network as MindSporeNetwork

network = MindSporeNetwork()
model = Model(network, loss_fn=loss_fn)
model.eval(dataset)

命令行参数说明

usage: mindconverter [-h] [--version]
                     [--model_file MODEL_FILE] [--shape SHAPE [SHAPE ...]]
                     [--input_nodes INPUT_NODES [INPUT_NODES ...]]
                     [--output_nodes OUTPUT_NODES [OUTPUT_NODES ...]]
                     [--output OUTPUT] [--report REPORT]

参数含义如下:

参数名 必填 功能描述 类型 默认值 取值示例
-h, --help 显示帮助信息 - - -
--version 显示版本信息 - - -
--model_file 指定模型文件路径 String - /path/to/model.onnx
--shape 指定模型输入shape信息,多输入场景以空格分隔 String - 1,3,224,224
--input_nodes 指定模型输入节点名称,多输入场景以空格分隔 String - input_1:0
--output_nodes 指定模型输出节点名称,多输出场景以空格分隔 String - output_1:0 output_2:0
--output 指定转换生成文件的保存目录 String $PWD /path/to/output/dir
--report 指定转换报告文件的保存目录 String $PWD /path/to/report/dir

模型支持列表

MindConverter已支持转换的模型列表,请参考链接

错误码速查表

MindConverter错误码定义,请参考链接

常见问题

ARM环境安装依赖组件

ARM环境下使用模型迁移工具,需要源码编译安装protobuf/onnx/onnxoptimizer,编译过程可能涉及其他系统组件,现编译报错需要人工排查,因此建议切换到x86环境中使用模型迁移工具。

  1. 源码编译安装protobuf(参考 ONNX)与cpp后端。

    # 编译安装 protobuf
    git clone https://github.com/protocolbuffers/protobuf.git
    cd protobuf
    git checkout v3.16.0
    git submodule update --init --recursive
    mkdir build_source
    cd build_source
    cmake ../cmake -Dprotobuf_BUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX=/usr/local/protobuf -DCMAKE_INSTALL_SYSCONFDIR=/etc -DCMAKE_POSITION_INDEPENDENT_CODE=ON -Dprotobuf_BUILD_TESTS=OFF -DCMAKE_BUILD_TYPE=Release
    make -j$(nproc)
    make install
    
    # 安装cpp后端
    cd ../python
    python setup.py install --cpp_implementation
    
  2. 设置protobuf环境变量。

    export PROTOBUF_PATH=/usr/local/protobuf
    export PATH=$PROTOBUF_PATH/bin:$PATH
    export PKG_CONFIG_PATH=$PROTOBUF_PATH/lib/pkgconfig
    export LD_LIBRARY_PATH=$PROTOBUF_PATH/lib:$LD_LIBRARY_PATH
    export LIBRARY_PATH=$PROTOBUF_PATH/lib:$LIBRARY_PATH
    export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=cpp
    
  3. 验证protobuf的cpp后端。

    from google.protobuf.internal import api_implementation
    print(api_implementation.Type())
    
  4. 安装protobuf后,需要通过源码编译方式,重新安装onnx,以保证onnx基于静态库编译的protobuf运行,详情参考 安装指引

    git clone https://github.com/onnx/onnx.git
    cd onnx
    git submodule update --init --recursive
    # prefer lite proto
    set CMAKE_ARGS=-DONNX_USE_LITE_PROTO=ON
    pip install -e .
    
  5. 源码编译安装onnxoptimizer,详情参考 安装指引

    git clone --recursive https://github.com/onnx/optimizer onnxoptimizer
    cd onnxoptimizer
    pip3 install -e .
    
  6. 安装onnxruntime

    pip install onnxruntime~=1.5.2
    

TensorFlow模型导出

Tensorflow模型导出PB文件,进而映射成ONNX算子,需要Tensorflow算子支持相应的ONNX算子,详情参考TensorflowONNX的算子列表。使用Keras构建模型的用户,可尝试如下方法进行导出:

TensorFlow 1.x版本

import tensorflow as tf
from tensorflow.python.framework import graph_io
from tensorflow.python.keras.applications.inception_v3 import InceptionV3

model = InceptionV3()
INPUT_NODES = [ipt.op.name for ipt in model.inputs]
OUTPUT_NODES = [opt.op.name for opt in model.outputs]

tf.keras.backend.set_learning_phase(0)
session = tf.keras.backend.get_session()
with session.graph.as_default():
    graph_inf = tf.graph_util.remove_training_nodes(session.graph.as_graph_def())
    graph_frozen = tf.graph_util.convert_variables_to_constants(session, graph_inf, OUTPUT_NODES)
    graph_io.write_graph(graph_frozen, logdir="/path/to/output/dir", name="model.pb", as_text=False)

print(f"Input nodes name: {INPUT_NODES}, output nodes name: {OUTPUT_NODES}")

TensorFlow 2.x版本

import tensorflow as tf
from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2
from tensorflow.keras.applications import InceptionV3

model = InceptionV3()
spec = tf.TensorSpec(model.inputs[0].shape, model.inputs[0].dtype)
full_model = tf.function(lambda x: model(x)).get_concrete_function(spec)
frozen_func = convert_variables_to_constants_v2(full_model)
frozen_func.graph.as_graph_def()
tf.io.write_graph(frozen_func.graph, logdir="/path/to/output/dir", name="model.pb", as_text=False)

TensorFlow不作为MindInsight明确声明的依赖库,若想使用基于图结构的模型转换工具,需要手动安装TensorFlow

整改forward参数列表

某些模型的forward参数列表中,包含非Tensor类型的入参,示例如下:

class Model(nn.Cell):
    def __init__(self, *args, **kwargs):
        self.op = Operator()
        self.loss_fn = LossFunction()
    def forward(self, data, label):
        output = self.op(data)
        loss = self.loss_fn(output, label)
        return output, loss

其中 label 不是Tensor类型的入参,需要进行整改:

class Model(nn.Cell):
    def __init__(self, *args, **kwargs):
        self.op = Operator()
    def forward(self, data):
        output = self.op(data)
        return output

MindSpore模型内嵌到原框架

将MindSpore的模型内嵌到PyTorch脚本中,结合权重信息,验证转换的等价性。

import torch
from torch.utils.data import DataLoader
import mindspore
from project.model import Network as MindSporeNetwork

network = MindSporeNetwork()
param_dict = mindspore.load_checkpoint('network.ckpt')
mindspore.load_param_into_net(network, param_dict)

data_loader = DataLoader(dataset, batch_size=BATCH_SIZE)
for data, label in data_loader:
    ms_data = mindspore.Tensor(data.numpy())
    ms_output = network(ms_data)
    output = torch.Tensor(ms_output.asnumpy())
    loss = loss_fn(output, label)

转换报告与权重映射表

对于未成功转换的算子,转换报告记录未转换的代码行与算子信息,同时在代码中标识该节点输入/输出的shape(分别表示为input_shapeoutput_shape),便于用户手动修改。以Reshape算子为例,将生成如下代码:

class Classifier(nn.Cell):
    def __init__(self):
        super(Classifier, self).__init__()
        self.reshape = onnx.Reshape(input_shape=(1, 1280, 1, 1), output_shape=(1, 1280))

    def construct(self, x):
        # Suppose input of `reshape` is x.
        reshape_output = self.reshape(x)
        # skip codes ...

通过input_shapeoutput_shape参数,用户可以便捷地完成算子替换,替换结果如下:

from mindspore import ops

class Classifier(nn.Cell):
    def __init__(self):
        super(Classifier, self).__init__()
        self.reshape = ops.Reshape(input_shape=(1, 1280, 1, 1), output_shape=(1, 1280))

    def construct(self, x):
        # Suppose input of `reshape` is x.
        reshape_output = self.reshape(x, (1, 1280))
        # skip codes ...

权重映射表保存算子在MindSpore中的权重信息(converted_weight)和在原始框架中的权重信息(source_weight),示例如下:

{
  "resnet50": [
    {
      "converted_weight": {
        "name": "conv2d_0.weight",
        "shape": [64, 3, 7, 7],
        "data_type": "Float32"
      },
      "source_weight": {
        "name": "conv1.weight",
        "shape": [64, 3, 7, 7],
        "data_type": "float32"
      }
    }
  ]
}

基于AST转换脚本

MindConverter支持基于AST的方案进行PyTorch脚本迁移,通过对原脚本的抽象语法树进行解析、编辑,将其替换为MindSpore的抽象语法树,再利用抽象语法树生成代码。

抽象语法树语法树解析操作受原脚本用户编码风格影响,可能导致同一模型的不同脚本最终的转换率存在一定差异,因此AST方案已调整为DEPRECATED状态,将在r2.0版本移除。

假设原PyTorch脚本路径为/path/to/model.py,用户希望将脚本输出至/path/to/output/dir,转换命令如下:

mindconverter --in_file /path/to/model.py --output /path/to/output/dir

转换报告中,对于未转换的代码行形式为如下,其中x, y指明的是原PyTorch脚本中代码的行、列号。对于未成功转换的算子,可参考MindSporeAPI映射查询功能手动对代码进行迁移。对于工具无法迁移的算子,会保留原脚本中的代码。

line x:y: [UnConvert] 'operator' didn't convert. ...

以下转换报告示例中,对于部分未成功转换的算子,转换报告提供修改建议:如line 157:23,将torch.nn.AdaptiveAvgPool2d替换为mindspore.ops.ReduceMean

 [Start Convert]
 [Insert] 'from mindspore import ops' is inserted to the converted file.
 line 1:0: [Convert] 'import torch' is converted to 'import mindspore'.
 ...
 line 157:23: [UnConvert] 'nn.AdaptiveAvgPool2d' didn't convert. Maybe could convert to mindspore.ops.ReduceMean.
 ...
 [Convert Over]

AST方案不支持以下场景:

  1. 部分类和方法目前无法转换。

    • 使用torch.Tensorshapendimdtype成员

    • torch.nn.AdaptiveXXXPoolXdtorch.nn.functional.adaptive_XXX_poolXd()

    • torch.nn.functional.Dropout

    • torch.unsqueeze()torch.Tensor.unsqueeze()

    • torch.chunk()torch.Tensor.chunk()

  2. 继承的父类是nn.Module的子类。

    # 代码片段摘自torchvision.models.mobilenet
    from torch import nn
    
    class ConvBNReLU(nn.Sequential):
       def __init__(self, in_planes, out_planes, kernel_size=3, stride=1, groups=1):
           padding = (kernel_size - 1) // 2
           super(ConvBNReLU, self).__init__(
               nn.Conv2d(in_planes, out_planes, kernel_size, stride, padding, groups=groups, bias=False),
               nn.BatchNorm2d(out_planes),
               nn.ReLU6(inplace=True)
           )