|本文转载自知乎 @嘿呀嘿 个人 blog。

1. 前语

Megengine 是旷视科技开发的一款练习推理一体化的深度学习结构,类似于 pytorch,tensorflow。

运用 Megengine 能够快速完结常见的深度学习模型,本文将运用 Megengine 完结手写数字识别,以完结深度学习的两大过程:练习和猜测。经过本文,读者对深度学习的最基本流程和 Megengine 结构的运用办法有大致了解。

2. 环境装置

在命令行输入下列句子即可装置 Megengine ,主张运用 python 版本为 3.5 到 3.8

python3 -m pip install --upgrade pip
python3 -m pip install megengine -f https://megengine.org.cn/whl/mge.html

装置完结后能够在命令行测验是否装置成功。

python3
import megengine
print(megengine.__version__)

3. 练习

本部分练习代码来自 Megengine 官方教程,需求详细了解细节请前往 MegEngine 快速上手

3.1 数据集预备

3.1.1 下载数据集

深度学习的第一步为预备数据集,通常会为数据集写一个接口来访问数据集,并对数据进行预处理。

Megengine 中已经完结了 MNIST 数据集的接口,咱们能够经过以下代码直接获取。假如想要制造或运用其他数据集,能够点击这儿进行学习。

from megengine.data.dataset import MNIST
DATA_PATH = "./datasets/MNIST"
#第一次运行后,将 download 改为 False
train_dataset = MNIST(DATA_PATH, train=True, download=True)
test_dataset = MNIST(DATA_PATH, train=False, download=True)

3.1.2 数据加载及预处理

上面运用 MNIST()完结数据集的加载和 Dataset 的构建,接下来将对数据进行加载,修正数据。运用 DataLoader、Sampler 和 Transform 完结。

DataLoader

功用: 构建可迭代的数据装载器,非常灵活地从数据集连续获取小批量数据
参数

  • dataset – 需求从中分批加载的数据集。
  • sampler (Optional) – 界说从数据会集采样数据的战略。
  • transform (Optional) – 界说抽样批次的转换战略。对数据需求作的改换 默许:None
RandomSampler

功用:创立一个列表,包括所有数据的索引,可完结数据的随机取样
参数

  • dataset – 待采样的方针数据集。
  • batch_size – 运用 batch 办法时指定 batch 巨细。
  • drop_last – 假如 batch 巨细不能整除数据集巨细时,为 True 则抛弃最终一个不完整的batch; 为 False 则最终一个batch可能比较小。默许:False
import megengine.data as data
import megengine.data.transform as T
train_sampler = data.RandomSampler(train_dataset, batch_size=64)
test_sampler = data.SequentialSampler(test_dataset, batch_size=4)
transform = T.Compose([
    T.Normalize(0.1307*255, 0.3081*255),
    T.Pad(2),
    T.ToMode("CHW"),
])
train_dataloader = data.DataLoader(train_dataset, train_sampler, transform)
test_dataloader = data.DataLoader(test_dataset, test_sampler, transform)

3.2 模型

接下来界说网络结构,LeNet 的网络结构如下图所示。
[图片上传失败…(image-41aaca-1660811354056)]
界说网络结构主要为两步:界说网络子模块和连接网络子模块。如下代码所示,运用 init 办法创立子模块,forward()办法连接子模块。

import megengine.functional as F
import megengine.module as M
class LeNet(M.Module):
    def __init__(self):
        super().__init__()
        #输入巨细为(batch, 1, 32, 32),输出巨细为(batch, 6, 28, 28)
        self.conv1 = M.Conv2d(1, 6, 5)
        self.conv2 = M.Conv2d(6, 16, 5)
        self.fc1   = M.Linear(16*5*5, 120)
        self.fc2   = M.Linear(120, 84)
        self.fc3   = M.Linear(84, 10)
    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), 2)
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = F.flatten(x, 1)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x   
  • class LeNet 继承自 Module 的类,想要了解更多关于构建模型的细节,能够参阅参阅 module 界说模型结构

3.3 练习预备

  • GradManager 负责核算梯度,管理需求优化的参数,用attach()办法增加,参加的参数才会被核算保存梯度值。对梯度核算了解更多请点击 Autodiff 基本原理与运用
  • optimizer 挑选运用的优化办法,这儿运用 Optimizer.SGD(随机梯度下降法)作为优化器,对优化器了解更多请点击运用 optimizer 优化参数
import megengine.optimizer as optim
import megengine.autodiff as autodiff
gm = autodiff.GradManager().attach(model.parameters())
#参数为需求优化的参数,学习率等
optimizer = optim.SGD(
    model.parameters(),
    lr=0.01,
    momentum=0.9,
    weight_decay=5e-4
)

3.4 练习迭代

接下来进入程序的主逻辑,开端练习模型。运用两个嵌套循环,一个大循环为一个 epoch,遍历一次数据集,核算一次准确度。

每个小循环为一个 batch,将一批数据传入模型中,进行前向核算得到猜测概率,运用交叉熵(cross_entropy)来核算 loss, 接着调用 GradManager.backward 办法进行反向核算而且记载每个 tensor 的梯度信息。然后运用 Optimizer.step 办法更新模型中的参数。因为每次更新参数后不主动铲除梯度,所以还需求调用 clear_grad 办法。

import megengine
epochs = 10
model.train()
for epoch in range(epochs):
    total_loss = 0
    for batch_data, batch_label in train_dataloader:
        batch_data = megengine.Tensor(batch_data)
        batch_label = megengine.Tensor(batch_label)
        with gm:
            logits = model(batch_data)
            loss = F.nn.cross_entropy(logits, batch_label)
            gm.backward(loss)
            optimizer.step().clear_grad()
        total_loss += loss.item()
    print(f"Epoch: {epoch}, loss: {total_loss/len(train_dataset)}")

3.5 保存模型

常用的神经网络都具有非常大数量级的参数,每次练习需求花费很长时刻,为了能够练习中止后能够依照上次练习的成果接着练习,咱们能够每10个 epoch 保存一次模型(或更多)。保存模型有几种办法,如表所示。办法详细介绍请点击保存与加载模型。

办法 好坏
保存/加载整个模型 任何情况都不推荐
保存加载模型状况字典 适用于推理,不满足恢复练习要求
保存加载检查点 适用于推理或恢复练习
导出静态图模型 适用于推理,寻求高性能布置
咱们挑选保存加载检查点,既能够用于恢复练习也能够推理。保存时调用 megengine.save()办法,参数如下:
megengine.save({
                "epoch": epoch,
                "state_dict": model.state_dict(),
                "optimizer_state_dict": optimizer.state_dict(),
                "loss": loss,
                ...
               }, PATH)

然后就能够愉快的进行练习了,调查练习成果,当 loss 下降到一定境地,准确率满足要求后,停止练习.

假如练习发生中止,能够调用 load()办法和 optimizer.load_state_dict()办法,对模型的加载,重新开端练习。代码如下:

model = LeNet()
optimizer = optim.SGD()
checkpoint = megengine.load(PATH)
model.load_state_dict(checkpoint["model_state_dict"])
optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
epoch = checkpoint["epoch"]
loss = checkpoint["loss"]
model.eval()
# - or -
model.train()

4. 推理

上面几个章节已经完结深度学习大部分内容,已经能够发生一个需求的算法模型。这个算法对预备好的数据集有比较好的拟合效果,可是咱们的最终意图是用模型进行推理,即能够对新的数据进行猜测。这将是下面介绍的内容。

首要有一种很简单的办法,运用 python 加载模型并设定 model.eval(),代码如下所示,这样就能够简单调用练习好的模型用以实践。

from train import LeNet
import cv2
import numpy as np
import megengine
import megengine.data.transform as T
import megengine.functional as F
IMAGE_PATH = "./test.png"
CHECK_POINT_PATH = "./checkpoint.pkl"
def load_model(check_point_path = CHECK_POINT_PATH):
    model  = LeNet()
    check_point = megengine.load(check_point_path)
    #留意checkpoint保存时模型对应的键,此处为state_dict
    model.load_state_dict(check_point["state_dict"])
    model.eval()
    return model
def main():
    # 加载一张图画为灰度图
    image = cv2.imread(IMAGE_PATH,cv2.IMREAD_GRAYSCALE)
    image = cv2.resize(image, (32, 32))
    #将图片改换为黑底白字
    image = np.array(255-image)
    tensor_image = megengine.tensor(image).reshape(1, 1, 32, 32)
    model = load_model()
    logit= model(tensor_image)
    pred = F.argmax(logit, axis=1).item()
    print("number:", pred)
if __name__ == "__main__":
    main()

不过在实践布置中,还需求考虑布置环境,推理速度等因素,所以从练习好模型到布置落地还有很长的路。Megengine 因为其设计特色——练习推理一体化,能够便利地将练习模型布置。这将是下一章介绍的内容,下一章将运用 C++ 调用 Megengine lite,进行高效布置。

参阅文献

[1]: MegEngine 快速上手
[2]: Yann LeCun, Corinna Cortes, and CJ Burges. Mnist handwritten digit database. ATT Labs [Online]. Available: yann.lecun.com/exdb/mnist, 2010.
[3]: Yann LeCun, Lon Bottou, Yoshua Bengio, and Patrick Haffner. Gradient-based learning applied to document recognition. Proceedings of the IEEE, 86(11):2278–2324, 1998.

附录:train.py

from megengine.data.dataset import MNIST
from megengine import jit, tensor
import megengine
import numpy as np
import megengine.data as data
import megengine.data.transform as T
import megengine.functional as F
import megengine.module as M
import megengine.optimizer as optim
import megengine.autodiff as autodiff
DATA_PATH = "./datasets/train/"
def load_data(data_path =DATA_PATH):
    train_dataset = MNIST(DATA_PATH)
    test_dataset = MNIST(DATA_PATH)
    train_sampler = data.RandomSampler(train_dataset, batch_size=64)
    test_sampler = data.SequentialSampler(test_dataset, batch_size=2)
    transform = T.Compose([
        T.Normalize(0.1307*255, 0.3081*255),
        T.Pad(2),
        T.ToMode("CHW"),
    ])
    train_dataloader = data.DataLoader(train_dataset, train_sampler, transform)
    test_dataloader = data.DataLoader(test_dataset, test_sampler, transform)
    return train_dataloader, test_dataloader
#Define model
class LeNet(M.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = M.Conv2d(1, 6, 5)
        self.conv2 = M.Conv2d(6, 16, 5)
        self.fc1   = M.Linear(16*5*5, 120)
        self.fc2   = M.Linear(120, 84)
        self.fc3   = M.Linear(84, 10)
    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), 2)
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = F.flatten(x, 1)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
def train(dataloader):
    model = LeNet()
    # GradManager and Optimizer setting
    gm = autodiff.GradManager().attach(model.parameters())
    optimizer = optim.SGD(
        model.parameters(),
        lr=0.01,
        momentum=0.9,
        weight_decay=5e-4
    )
    # Training and validation
    nums_epoch = 50
    for epoch in range(nums_epoch):
        training_loss = 0
        nums_train_correct, nums_train_example = 0, 0
        nums_val_correct, nums_val_example = 0, 0
        for step, (image, label) in enumerate(dataloader[0]):
            image = megengine.Tensor(image)
            label = megengine.Tensor(label)
            with gm:
                score = model(image)
                loss = F.nn.cross_entropy(score, label)
                gm.backward(loss)
                optimizer.step().clear_grad()
            training_loss += loss.item() * len(image)
            pred = F.argmax(score, axis=1)
            nums_train_correct += (pred == label).sum().item()
            nums_train_example += len(image)
        training_acc = nums_train_correct / nums_train_example
        training_loss /= nums_train_example
        for image, label in dataloader[1]:
            image = megengine.Tensor(image)
            label = megengine.Tensor(label)
            pred = F.argmax(model(image), axis=1)
            nums_val_correct += (pred == label).sum().item()
            nums_val_example += len(image)
        val_acc = nums_val_correct / nums_val_example
        #每十次epoch保存一次模型
        if epoch%2 == 0:
            megengine.save(
                    {"epoch":epoch, 
                    "state_dict": model.state_dict(),
                    "optimizer_state_dict": optimizer.state_dict(),
                    "loss": loss,
                    }, 
                    "./checkpoint.pkl")
        print(f"Epoch = {epoch}, "
            f"train_loss = {training_loss:.3f}, "
            f"train_acc = {training_acc:.3f}, "
            f"val_acc = {val_acc:.3f}")
def dumpy_mge(pkl_path = "checkpoint.pkl"):
    model = LeNet()
    check_point = megengine.load(pkl_path)
    model.load_state_dict(check_point["state_dict"])
    model.eval()
    @jit.trace(symbolic=True, capture_as_const=True)
    def infer_func(input, *, model):
        pred  = model(input)
        return pred
    input = megengine.Tensor(np.random.randn(1, 1, 32, 32))
    output = infer_func(input, model=model)
    infer_func.dump("./lenet.mge", arg_names=["input"])
if __name__=='__main__':
    train(load_data())

C++ 推理

前半部分咱们完结了深度学习的练习,得到了LeNet练习权重文件,后边咱们将运用练习权重文件导出静态图模型。并运用C++调用模型完结实践布置的模仿。

预备工作

在上一章中,咱们说到有四种保存模型的办法,如下表所示,为了练习便利起见,保存了checkpoint文件。但实践布置中咱们经常运用静态图模型,所以咱们首要要完结静态图导出。

办法 好坏
保存/加载整个模型 任何情况都不推荐
保存加载模型状况字典 适用于推理,不满足恢复练习要求
保存加载检查点 适用于推理或恢复练习
导出静态图模型 适用于推理,寻求高性能布置

处处静态图在megengine中有较完整的教程,请参阅导出序列化模型文件(Dump)。主要分为三步:

  1. 将循环内的前向核算、反向传播和参数优化代码提取成独自的函数,如下面例子中的 train_func()
  2. 将网络所需输入作为练习函数的参数,并回来恣意你需求的成果(如输出成果、损失函数值等);
  3. 用 jit 模块中的 trace 装修器来装修这个函数,将其间的代码变为静态图代码。

在上一章最终的附录train.py中有dump静态图的办法,代码如下:


from megengine import jit
def dump_mge(pkl_path = "checkpoint.pkl"):
    model = LeNet()
    check_point = megengine.load(pkl_path)
    model.load_state_dict(check_point["state_dict"])
    model.eval()
    @jit.trace(symbolic=True, capture_as_const=True)
    def infer_func(input, *, model):
        pred  = model(input)
        pred_normalized = F.softmax(pred)
        return pred_normalized
    input = megengine.Tensor(np.random.randn(1, 1, 32, 32))
    output = infer_func(input, model=model)
    infer_func.dump("./lenet.mge", arg_names=["input"])

调用dump_mge办法即可完结静态图导出。

inference代码

代码的主逻辑为:

  1. 创立Network
  2. 运用load_model()载入模型
  3. 运用stb预处理图片(加载和resize),然后归一化,载入进input tensor
  4. 运用network->forward()和network->wait()完结推理逻辑。
  5. 获取模型输出tensor,并对其进行处理。

推理代码为:

//inference.cpp
#include <iostream>
#include <stdlib.h>
#define STB_IMAGE_IMPLEMENTATION
#include "stb/stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb/stb_image_write.h"
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#define STB_IMAGE_RESIZE_STATIC
#include "stb/stb_image_resize.h"
#include "lite/network.h"
#include "lite/tensor.h"
//留意在这儿修正测验图片与所用模型
#define IMAGE_PATH "./test.png"
#define MODEL_PATH "./lenet.mge"
void preprocess_image(std::string pic_path, std::shared_ptr<lite::Tensor> tensor) {
    int width, height, channel;
    uint8_t* image = stbi_load(pic_path.c_str(), &width, &height, &channel, 0);
    printf("Input image %s with height=%d, width=%d, channel=%d\n", pic_path.c_str(),
           width, height, channel);
    auto layout = tensor->get_layout();
    auto pixels = layout.shapes[2] * layout.shapes[3];
    size_t image_size = width * height * channel;
    size_t gray_image_size = width * height * 1;
    unsigned char *gray_image = (unsigned char *)malloc(gray_image_size);
    for(unsigned char *p=image, *pg=gray_image; p!=image+image_size; p+=channel,pg++)
    {
        *pg = uint8_t(*p + *(p+1) + *(p+2))/3.0;
    }
    //! resize to tensor shape
    std::shared_ptr<std::vector<uint8_t>> resize_int8 =
            std::make_shared<std::vector<uint8_t>>(pixels * 1);
    stbir_resize_uint8(
            gray_image, width, height, 0, resize_int8->data(), layout.shapes[2],
            layout.shapes[3], 0, 1);
    free(gray_image);
    stbi_image_free(image);
    //! 减去均值,归一化
    unsigned int sum = 0;
    for(unsigned char *p=gray_image; p!=gray_image+gray_image_size;p++){
    sum += *p;
    }
    sum /= gray_image_size;
    float* in_data = static_cast<float*>(tensor->get_memory_ptr());
    for (size_t i = 0; i < pixels; i++) {
        in_data[i] = resize_int8->at(i)-sum;     
    }
}
int main()
{
    //创立网络
    std::shared_ptr<lite::Network> network = std::make_shared<lite::Network>();
    //加载模型
    network->load_model(MODEL_PATH);
    std::shared_ptr<lite::Tensor> input_tensor = network->get_io_tensor("input");
    preprocess_image(IMAGE_PATH, input_tensor);
    //将图片转为Tensor
    network->forward();
    network->wait();
    std::shared_ptr<lite::Tensor> output_tensor = network->get_output_tensor(0);
    float* predict_ptr = static_cast<float*>(output_tensor->get_memory_ptr());
    float max_prob = predict_ptr[0];
    size_t number = 0;
    //寻找最大的标签
    for(size_t i=0; i<10; i++)
    {
        float cur_prob = predict_ptr[i];
        if(cur_prob>max_prob)
        {
            max_prob = cur_prob;
            number = i;
        }
    }
    std::cout << "the predict number is :" << number << std::endl;
    return 0;
}

推理的代码已经编写完结,还需求对其进行编译,依据咱们布置的渠道,挑选编译方式,比如安卓,能够挑选交叉编译。这儿咱们挑选布置在本机上。

能够运用g++进行编译,编译时需求连接MegEngine Lite库文件,而且预备好stb头文件。

装备环境

因为运用C++调用MegEngine Lite接口,所以咱们首要需求编译出MegEngine Lite的库。
装置MegEngine:从源代码编译MegEngine。请参阅编译MegEngine Lite。

  1. clone MegEngine工程,进入根目录
git clone --depth=1 git@github.com:MegEngine/MegEngine.git
cd MegEngine
  1. 装置MegEngine所需的依赖
./third_party/prepare.sh
./third_party/install-mkl.sh
  1. 运用cmake进行编译工程得到c++推理所需的库文件
scripts/cmake-build/host_build.sh

编译完结后,需求的库文件所在地址为:

MegEngine/build_dir/host/MGE_WITH_CUDA_OFF/MGE_INFERENCE_ONLY_ON/Release/install/lite/

这儿为了在g++编译时增加库文件便利,能够将库文件地址设为环境变量

export LITE_INSTALL_DIR=/path/to/megenginelite-lib #上一步中编译生成的库文件装置途径
export LD_LIBRARY_PATH=$LITE_INSTALL_DIR/lib/x86_64/:$LD_LIBRARY_PATH

装置stb
stb是一个轻量化的图片加载库,能够代替opencv完结图片的解码。想要运用它,只需求将对应的头文件包括到项目内,不像opencv需求编译发生链接库。

这儿为了调用便利直接将stb的项目下载下来:

git clone https://github.com/nothings/stb.git

想要运用图片加载函数stbi_load(),只需在cpp文件中define STB_IMAGE_IMPLEMENTATION而且include stb_image.h头文件

#define STB_IMAGE_IMPLEMENTATION
#include "stb/stb_image.h"

动态链接编译

最终运用g++或许clang完结对inference.cpp的编译。

  • -I选项增加编译时头文件查找途径
  • -l增加动态链接库
  • -L增加动态链接库查找途径
g++ -o inference -I$LITE_INSTALL_DIR/include -I./stb inference.cpp -llite_shared -L$LITE_INSTALL_DIR/lib/x86_64

编译后会在本目录下会得到inference二进制文件

履行二进制文件

预备好一张手写数字图片,将图片与模型放到同一目录,履行编译好的文件即可得到推理成果。

./inference test.jpg

以上就完结了LeNet神经网络的布置。

更多 MegEngine 信息获取,您能够:
查看MegEngine 官网和GitHub 项目,或参加 MegEngine 用户沟通 QQ 群:1029741705