PyTorch迁移MindSpore学习记录(上)
收藏回复举报
PyTorch迁移MindSpore学习记录(上)
新人帖
发表于2023-08-28 14:23:32
0 查看

PyTorch迁移MindSpore学习记录(上)

研一的时候学习深度学习,一开始学的是PyTorch,确实挺好用的,也基于PyTorch实现了很多算法。

研一暑假突发兴趣学习一下国产框架MindSpore,看到一些资料说已经迭代到2.0了,API也比较类似PyTorch,特来学习一下。

由于我比较熟悉PyTorch,我计划把一个简单的PyTorch网络迁移到MindSpore,顺带学习下API是怎么使用的。

PyTorch小网络

由于不知道迁移起来麻不麻烦,我先搭了一个类LeNet的网络,层数比较少,对照起来也方便。

在数据加载方面,我用DataLoader读取了本地的4张图片(取自ImageNet),本地的目录结构如下:

root@root:mindspore# tree images/
images/
├── class1
│   └── n10148035_108.JPEG
├── class2
│   └── n11879895_10055.JPEG
└── class3
    └── n13052670_1088.JPEG

总体上,就是加载了4张图片作为数据集,然后图片统一resize成(32,32)传到网络里,然后网络进行2次卷积和2次全连接,最后计算一下softmax,再把结果与label计算一下交叉熵损失,最后bp一下。

from PIL import Image
import numpy as np

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.optim.lr_scheduler import StepLR
from torch.utils.data import DataLoader
from torch.utils.data import Dataset
from torchvision import transforms


class ImageDataset(Dataset):
    def __init__(self, file_paths, labels):
        super(ImageDataset, self).__init__()
        self.file_paths = file_paths
        self.labels = labels
    
    def __len__(self):
        return len(self.file_paths)
    
    def __getitem__(self, idx):
        # [h, w, c]
        img = Image.open(self.file_paths[idx]).convert('RGB')
        # [c, h, w]
        img = transforms.ToTensor()(img)
        # [c, 32, 32]
        img = transforms.Resize((32, 32))(img)
        label = self.labels[idx]

        return {"image": img, "label": label}

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 32, 3, 1)
        self.conv2 = nn.Conv2d(32, 64, 3, 1)
        self.fc1 = nn.Linear(12544, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        x = F.max_pool2d(x, 2)
        x = torch.flatten(x, 1)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        output = F.log_softmax(x, dim=1)
        return output

dataset = ImageDataset(["./images/class1/n10148035_108.JPEG",
                        "./images/class2/n11879895_10055.JPEG",
                        "./images/class3/n13052670_1088.JPEG",
                        "./images/class3/n13052670_1088.JPEG"],
                        [0, 1, 2, 2])

dataloader = DataLoader(dataset, batch_size=2)
model = Net()

optimizer = optim.Adadelta(model.parameters(), lr=0.001)
scheduler = StepLR(optimizer, step_size=1, gamma=0.7)

epochs = 3
for epoch in range(1, epochs+1):
    model.train()
    for batch_idx, item in enumerate(dataloader):
        optimizer.zero_grad()
        output = model(item["image"])
        loss = F.nll_loss(output, item["label"])
        loss.backward()
        optimizer.step()
        print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, (batch_idx+1) , len(dataloader),
                100. * (batch_idx+1) / len(dataloader), loss.item()))

运行结果如下

Train Epoch: 1 [1/2 (50%)]      Loss: 2.292146
Train Epoch: 1 [2/2 (100%)]     Loss: 2.324631
Train Epoch: 2 [1/2 (50%)]      Loss: 2.287233
Train Epoch: 2 [2/2 (100%)]     Loss: 2.320507
Train Epoch: 3 [1/2 (50%)]      Loss: 2.282384
Train Epoch: 3 [2/2 (100%)]     Loss: 2.316641

一共4张图片,batch size为2,。因此训练3个epoch,每个epoch训练2个batch的数据,loss收不收敛这个就忽略吧,毕竟就几张图片,也没训几轮。那么接下来开始迁移MindSpore。

小网络迁移到MindSpore

数据集迁移

查了一些MindSpore官网的资料,PyTorch的DataLoader跟MindSpore的GeneratorDataset用法还是很像的,也是定义一个dataset类,传入到DataLoader里(MindSpore是传到GeneratorDataset里),而dataset的定义不能说毫不相同,只能说一模一样。

我就大概照着写了写就实现了:

import numpy as np
from PIL import Image
import mindspore.dataset as ds
import mindspore.dataset.vision as vision


class ImageDataset:
    def __init__(self, file_paths, labels):
        self.file_paths = file_paths
        self.labels = labels
    
    def __len__(self):
        return len(self.file_paths)
    
    def __getitem__(self, idx):
        img = Image.open(self.file_paths[idx]).convert('RGB')
        img = vision.Resize((32, 32))(img)
        img = vision.ToTensor()(img)
        label = self.labels[idx]

        return {"image": img, "label": label}

dataset = ImageDataset(["./images/class1/n10148035_108.JPEG",
                        "./images/class2/n11879895_10055.JPEG",
                        "./images/class3/n13052670_1088.JPEG",
                        "./images/class3/n13052670_1088.JPEG"],
                        [0, 1, 2, 3])
dataloader = ds.GeneratorDataset(dataset, ["data"]).batch(2)

关于怎么验证数据对不对,我也大概查了查资料,就类似PyTorch那样迭代DataLoader对象就好了。

for d in dataloader:
    data = d[0]["image"]
    label = d[0]["label"]
    print(data.shape, label)
(2, 3, 32, 32) [2 3]
(2, 3, 32, 32) [1 0]

第一步接入数据集就做完了,整体感觉API还是挺类似的,体验也还行。

参考资料

填坑

  • 当然在迁移数据集的时候,还有遇到了一个坑:PyTorch的图像数据处理torchvision,要求输入图像是(c, h, w)格式的,因此在__getitem__里,torch的写法会先调用transforms.ToTensor()(img)把图像转为(c, h, w)格式,再传入到transforms.Resize((32, 32))中,这样维度才对的上,计算才是正确的。而MindSpore的vision数据处理,是要求输入图像是(h, w, c)格式的,因此写法上需要先调用vision.Resize((32, 32))(img)把图像尺寸调整完,再调用vision.ToTensor()(img)把格式转为(c, h, w)格式传到网络里。这里踩坑踩了一会,但是其实数据可以直接for循环迭代调试,所以整体难度不高。

  • 迭代数据集的时候,也就是从for d in dataloader拿到的输出d,其实是个tuple,他不是__getitem__返回的字典本身,这里一开始会有点不习惯,后面习惯了还好,其实d就是tuple(dict,)这样的数据,把值取出来就好了。

网络迁移

网络迁移,整体上API相似度还是蛮高的,我感觉对应关系和修改量就是

import torch.nn as nn  ->  import mindspore.nn as nn
import torch.nn.functional as F  -> from mindspore import ops as F
nn.Module -> nn.Cell
算子入参有一些微调

网络结构上差不大多,我是这么定义的

import mindspore as ms
import mindspore.nn as nn
from mindspore import ops as F

# 由于我有Ascend环境,所以手动设置了一下硬件后端
ms.set_context(device_target="Ascend")

# nn.Module -> nn.Cell
class Net(nn.Cell):
    def __init__(self):
        super(Net, self).__init__()
        # conv param mapping
        # 参考cid:link_4
        self.conv1 = nn.Conv2d(3, 32, 3, 1, has_bias=True, pad_mode='valid')
        self.conv2 = nn.Conv2d(32, 64, 3, 1, has_bias=True, pad_mode='valid')
        # Linear to Dense
        # 参考cid:link_5
        self.fc1 = nn.Dense(12544, 128)
        self.fc2 = nn.Dense(128, 10)

    # forward 改成 construct
    def construct(self, x):
        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        # cast to fp16
        x = F.cast(x, ms.float16)
        x = F.max_pool2d(x, 2)
        # cast back to fp32
        x = F.cast(x, ms.float32)
        x = F.flatten(x)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        # log_softmax的参数从dim 改成 axis
        output = F.log_softmax(x, axis=1)
        return output

网络就迁移完了,API相似度和结构都大差不多。测试下网络输出:

model = Net()
x = ms.Tensor(np.arange(2 * 3 * 32 * 32).reshape((2, 3, 32, 32)), ms.float32)
print(model(x).shape)
# 维度对的上ok
(2, 10)

参考资料

填坑

  • 改完网络之后随便输入了不同维度的数据到网络中,由于fc的参数不匹配,其实会导致计算错误,这个时候PyTorch的报错信息会直观一些,比如他会提示 RuntimeError: mat1 and mat2 shapes cannot be multiplied (2x12544 and 12534x128),直接就告诉说2个相乘的矩阵是什么维度。而MindSpore的日志是这样的 ValueError: For 'MatMul' the input dimensions must be equal, but got 'x1_col': 12544 and 'x2_row': 12534.,感觉不是很直观相乘的2个矩阵是什么维度。

  • max_pool2d在Ascend上只支持float16,所以网络修改在max_pool2d之前只能手动转为float16,在计算完在转回float32

=================================================================

迁移PyTorch网络到MindSpore的的进度完成了一半,数据集和网络都完成了迁移,今天就先梳理一下学习到的知识点,下次再把训练跑起来

=================================================================

本帖最后由 匿名用户2025/04/29 14:25:05 编辑

我要发帖子