【MindSpore】使用ms框架进行深度学习模型训练及理解函数式自动微分

目录

模型训练

构建数据集

定义神经网络模型

定义超参、损失函数和优化器

训练与评估

函数式自动微分

为什么 “函数输出多项时,微分函数会求所有输出项对参数的导数”?

为什么 “用 stop_gradient 可以屏蔽 z 对梯度的影响”?

关键对比:梯度如何变化?

求得w、b对应的梯度值与初始function求得的梯度值一致,同时z能够作为微分函数的输出返回。怎么理解?

辅助数据(Auxiliary Data)的本质

默认情况下(无has_aux)的梯度计算问题

has_aux=True的作用:自动屏蔽辅助数据的梯度

model是mindspore的Model类实例,不是自带predict方法吗,为什么要再自己写predict方法?

Model.predict 的核心功能

为什么需要自定义 predict 函数?

预处理:将原始输入转换为模型可接受的格式

后处理:将模型原始输出转换为业务结果

封装业务逻辑,提升易用性

Model.predict 与自定义 predict 的关系


参考内容:昇思MindSpore | 全场景AI框架 | 昇思MindSpore社区官网华为自研的国产AI框架,训推一体,支持动态图、静态图,全场景适用,有着不错的生态

本项目可以在华为云modelart上租一个实例进行,也可以在配置至少为单卡3060的设备上进行

https://console.huaweicloud.com/modelarts/

Ascend环境也适用,但是注意修改device_target参数

需要本地编译器的一些代码传输、修改等可以勾上ssh远程开发

模型训练

深度学习任务种,模型的训练通常分为:

1.构建数据集;

2.定义神经网络模型;

3.定义超参、损失函数及优化器;

4.输入数据集进行训练与评估

构建数据集

在此之前,环境没有‘download’,需要使用pip下载

!pip install download

datapipe’函数,适用于图像数据,这里下载开源手写数字的数据集MNIST_Data

import mindspore
from mindspore import nn
from mindspore.dataset import vision, transforms
from mindspore.dataset import MnistDataset

# 下载开源数据集
from download import download

url = "https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/" \
      "notebook/datasets/MNIST_Data.zip"
path = download(url, "./", kind="zip", replace=True)


def datapipe(path, batch_size):
    image_transforms = [
        vision.Rescale(1.0 / 255.0, 0),
        vision.Normalize(mean=(0.1307,), std=(0.3081,)),
        vision.HWC2CHW()
    ]
    label_transform = transforms.TypeCast(mindspore.int32)

    dataset = MnistDataset(path)
    dataset = dataset.map(image_transforms, 'image')
    dataset = dataset.map(label_transform, 'label')
    dataset = dataset.batch(batch_size)
    return dataset

# 调用数据集下载函数,传入指定路径
train_dataset = datapipe('MNIST_Data/train', batch_size=64)
test_dataset = datapipe('MNIST_Data/test', batch_size=64)

该数据集包含0-9十种数字的书写图片7万余张

查看一下数据集的情况

print(len(train_dataset),len(test_dataset))

Q:print(len(train_dataset),len(test_dataset))

得到:938 157

结合

# 超参

epochs = 3

batch_size = 64

learning_rate = 1e-2

所以推测训练集图片总量64*938?

A:批次大小(Batch Size):batch_size = 64

训练集的总批次数量:len(train_dataset) = 938

训练集的图片总量可以通过将批次大小乘以总批次数量来计算:

训练集图片总量=批次大小×总批次数量=64×938

计算结果:

64×938=60032

做一下可视化

import matplotlib.pyplot as plt
import numpy as np

# 题目1-3-1:利用create_dict_iterator API创建数据迭代器,并打印label列表
data_iter = train_dataset.create_dict_iterator()  

batch = next(data_iter)
images = batch["image"].asnumpy()
labels = batch["label"].asnumpy()
print(f"Image shape: {images.shape}, Label: {labels}")

plt.figure(figsize=(12, 5))
for i in range(24):
    plt.subplot(3, 8, i+1)
    # 利用transpose接口将通道维度移动到最后:CHW -> HWC
    image_trans = np.transpose(images[i], (1,2,0))
    mean = np.array([0.4914, 0.4822, 0.4465])
    std = np.array([0.2023, 0.1994, 0.2010])
    # 反归一化操作:利用std和mean对image_trans进行反归一化运算
    image_trans = image_trans*std + mean
    image_trans = np.clip(image_trans, 0, 1)

    plt.imshow(image_trans)
    plt.axis("off")
plt.show()

随机创建一个批次来看看数据图片

定义神经网络模型

定义一个简单的前馈神经网络,由三个全连接层构成,用ReLU作为激活函数

# 定义神经网络模型
class Network(nn.Cell):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.dense_relu_sequential = nn.SequentialCell(
            nn.Dense(28*28, 512),
            nn.ReLU(),
            nn.Dense(512, 512),
            nn.ReLU(),
            nn.Dense(512, 10)
        )

    def construct(self, x):
        x = self.flatten(x)
        logits = self.dense_relu_sequential(x)
        return logits

model = Network()

定义超参、损失函数和优化器

# 超参
epochs = 3
batch_size = 64
learning_rate = 1e-2

训练与评估

分类任务使用交叉熵损失函数

loss_fn = nn.CrossEntropyLoss()

优化器 获取可训练的参数model.trainable_params() 优化器的作用是管理需要更新的参数、定义参数的更新方式

optimizer = nn.SGD(model.trainable_params(), learning_rate=learning_rate)

训练与评估

# 前向传播函数 forward_fn
def forward_fn(data, label):
    logits = model(data)
    loss = loss_fn(logits, label)
    return loss, logits

# 梯度获取 value_and_grad();参数:optimizer.parameters -- 用于指定需要计算梯度的参数,这些参数通常是模型的权重
grad_fn = mindspore.value_and_grad(forward_fn, None, optimizer.parameters, has_aux=True)

# 定义每一轮中每一步的训练规则
def train_step(data, label):
    (loss, _), grads = grad_fn(data, label)
    optimizer(grads)
    return loss

# 定义循环训练函数
def train_loop(model, dataset):
    # 获取数据集规模
    size = dataset.get_dataset_size()
    # 默认开启训练模式
    model.set_train()
    for batch, (data, label) in enumerate(dataset.create_tuple_iterator()):
        # 对数据集的每个批次进行迭代
        loss = train_step(data, label)
        # 每100个批次打印一次损失值
        if batch % 100 == 0:
            loss, current = loss.asnumpy(), batch
            print(f"loss: {loss:>7f}  [{current:>3d}/{size:>3d}]")

test_loop函数同样需循环遍历数据集,调用模型计算loss和Accuray并返回最终结果

def test_loop(model, dataset, loss_fn):
    # 获取数据集规模
    num_batches = dataset.get_dataset_size()
    # 关闭训练模式
    model.set_train(False)
    # 初始化变量,用于统计总样本数、总损失和正确预测的样本数
    total, test_loss, correct = 0, 0, 0
    # 遍历数据集中的每个批次
    for data, label in dataset.create_tuple_iterator():
        # 使用模型对当前批次的数据进行预测
        pred = model(data)
        # 累加当前批次的样本数到总样本数
        total += len(data)
        # 计算当前批次的损失值,并累加到总损失中
        test_loss += loss_fn(pred, label).asnumpy()
        # 计算当前批次中正确预测的样本数,并累加到总正确数
        correct += (pred.argmax(1) == label).asnumpy().sum()
    # 计算平均损失
    test_loss /= num_batches
    # 计算准确率
    correct /= total
    print(f"Test: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")

开始训练 -- 将实例化的损失函数和优化器传入train_loop和test_loop中。训练3轮并输出loss和Accuracy,查看性能变化

训练循环+每轮test指标

for t in range(epochs):
    # 打印当前轮次的编号
    print(f"Epoch {t+1}\n-------------------------------")
    # 调用训练循环函数,对训练数据集进行训练
    train_loop(model, train_dataset)
    # 调用测试循环函数,对测试数据集进行评估
    test_loop(model, test_dataset, loss_fn)
print("Done!")

函数式自动微分

使用grad获得微分函数是一种函数变换,即输入为函数,输出也为函数。

'grad_fn = mindspore.grad(function, (2, 3))'

执行微分函数,即可获得w、b对应的梯度。

'

grads = grad_fn(x, y, w, b)

print(grads)

'

‘此时如果想要屏蔽掉z对梯度的影响,即仍只求参数对loss的导数,可以使用ops.stop_gradient接口,将梯度在此处截断’的意思?

为什么 “函数输出多项时,微分函数会求所有输出项对参数的导数”?

自动微分的本质是计算输出对参数的梯度,而梯度是累加的。当函数输出多个值时(比如同时输出loss和z),MindSpore 的grad函数会默认计算所有输出对参数的梯度之和。

在第一个函数function_with_logits中:

def function_with_logits(x, y, w, b):
    z = ops.matmul(x, w) + b  # z = x*w + b(线性变换)
    loss = ops.binary_cross_entropy_with_logits(z, y, ...)  # loss依赖z
    return loss, z  # 输出两个值:loss和z

当用grad函数求梯度时,grad_fn会计算:

loss对w、b的梯度(记为grad_loss)

z对w、b的梯度(记为grad_z)

最终的梯度是两者的累加(因为梯度是线性的,总梯度 = grad_loss + grad_z)。这就是为什么示例中第一次打印的梯度值较大(同时包含了loss和z的贡献)。

为什么 “用 stop_gradient 可以屏蔽 z 对梯度的影响”?

ops.stop_gradient(z)的作用是:在反向传播时,将z视为一个常数。即,在计算梯度时,z对参数(w、b)的梯度会被强制设为 0,不会参与累加。

在第二个函数function_stop_gradient中:

def function_stop_gradient(x, y, w, b):
    z = ops.matmul(x, w) + b
    loss = ops.binary_cross_entropy_with_logits(z, y, ...)
    return loss, ops.stop_gradient(z)  # z被标记为“停止梯度”

此时,grad函数只会计算loss对w、b的梯度(grad_loss),而z的梯度(grad_z)会被截断(变为 0)。因此总梯度 = grad_loss + 0 = grad_loss,这就相当于 “只保留loss对参数的梯度”。

tip:类似于对loss求偏导数,其他无关的变量视为常量,求导时后就是0

关键对比:梯度如何变化?

不使用 stop_gradient:总梯度 = loss的梯度 + z的梯度(示例中第一次打印的梯度较大)。

使用 stop_gradient:总梯度 = loss的梯度 + 0(示例中第二次打印的梯度与 “初始只输出 loss 时的梯度” 一致)。

求得w、b对应的梯度值与初始function求得的梯度值一致,同时z能够作为微分函数的输出返回。怎么理解?

辅助数据(Auxiliary Data)的本质

在深度学习中,模型的输出通常包含两部分:

主输出:训练的核心目标(如loss),需要通过梯度下降优化;

辅助数据:模型运行过程中产生的中间结果(如z),可能用于日志记录、可视化或后续分析,但不参与参数更新(即不希望影响梯度);

示例中,function_with_logits的输出是(loss, z),其中loss是主输出,z是辅助数据。

默认情况下(无has_aux)的梯度计算问题

当函数输出多个值时,MindSpore 的grad函数会默认计算所有输出对参数的梯度之和。

def function_with_logits(x, y, w, b):
    z = ops.matmul(x, w) + b  # z = x*w + b(依赖参数w/b)
    loss = ops.binary_cross_entropy_with_logits(z, y, ...)  # loss依赖z(间接依赖w/b)
    return loss, z  # 输出:(主输出loss, 辅助数据z)

此时,grad函数会计算:

loss对w/b的梯度(记为grad_loss,目标梯度)

z对w/b的梯度(记为grad_z,非目标梯度)

最终的梯度是两者的累加(总梯度 = grad_loss + grad_z)。这会导致梯度被辅助数据z“污染”,无法仅通过loss优化参数

has_aux=True的作用:自动屏蔽辅助数据的梯度

has_aux=True的核心逻辑是告诉框架:“函数的第一个输出是主目标(需要计算梯度),其他输出是辅助数据(不需要计算梯度)”。

框架会自动:

保留辅助数据:将辅助数据(如z)作为输出返回,不影响其前向计算;

屏蔽辅助数据的梯度:在反向传播时,仅计算主输出(loss)对参数的梯度,辅助数据(z)的梯度被视为 0(相当于隐式对z调用了stop_gradient)。

model是mindspore的Model类实例,不是自带predict方法吗,为什么要再自己写predict方法?

在 MindSpore 中,Model 类确实提供了 predict 方法(如 model.predict()),但自定义 predict 函数是实际工程中的常见做法。两者的核心区别在于:Model.predict 是框架提供的通用推理接口,而自定义 predict 函数是业务逻辑的封装,用于将原始输入(如文本)转换为模型可处理的格式,并将模型输出转换为用户需要的业务结果(如 “积极”/“消极” 标签)。

Model.predict 的核心功能

Model.predict 是 MindSpore Model 类提供的通用推理方法,

其核心作用是:

接收模型所需的输入张量(Tensor),执行前向计算,返回模型的原始输出(如 Logits、特征图等)。

例如,假设模型输入是 input_ids(形状为 [batch_size, seq_len] 的张量),则 model.predict(input_ids) 会直接返回模型的原始输出(如分类任务的 Logits)。

为什么需要自定义 predict 函数?

原始文本(如输入的 “这部电影真棒!”)无法直接输入模型,必须经过预处理(如分词、转换为 Token ID);模型的原始输出(如 Logits)也需要后处理(如取最大值、映射到标签)才能得到我们需要的结果(如 “积极”)。这些步骤需要通过自定义函数完成,而 Model.predict 本身不包含这些逻辑。

预处理:将原始输入转换为模型可接受的格式

自然语言处理(NLP)任务中,模型的输入通常是分词后的 Token ID 序列(如 BERT 的input_ids),而用户提供的原始文本(如字符串)无法直接输入模型。自定义 predict 函数的核心作用之一是:

将原始文本通过分词器(Tokenizer)转换为模型需要的输入张量。

例如

text_tokenized = Tensor([tokenizer(text).input_ids])  # 填空30: 输入文本

作用:使用分词器将原始文本 text 转换为 Token ID 序列(如 [101, 2023, 326, ..., 102]),并封装为 MindSpore 的Tensor(模型输入要求)。

不可替代性:Model.predict 不包含分词逻辑,必须通过自定义函数完成。

后处理:将模型原始输出转换为业务结果

模型的原始输出(如分类任务的 Logits)是数值张量(如 [0.1, 0.9]),用户需要的是具体的标签(如 “积极”)。自定义 predict 函数的另一核心作用是:

对模型输出进行解析,映射到业务标签。

例如

predict_label = logits[0].asnumpy().argmax()  # 取最大值的索引(如0或1)
return label_map[predict_label]  # 映射到“消极”或“积极”

作用:将模型输出的 Logits 转换为具体的标签名称。

不可替代性:Model.predict 仅返回原始输出,标签映射需通过自定义逻辑实现。

封装业务逻辑,提升易用性

自定义 predict 函数可以将 “分词→输入转换→模型推理→结果解析” 的全流程封装为一个接口,用户只需传入原始文本即可得到最终结果(如 “积极”),而无需关心中间细节。这在实际业务中(如 Web 服务、CLI 工具)非常关键。

例如,调用自定义 predict 函数的代码是:

result = predict("这部电影真棒!", model, tokenizer)
print(result)  # 输出:"积极"

而直接使用 Model.predict 则需要:

# 手动完成预处理
text_tokenized = Tensor([tokenizer("这部电影真棒!").input_ids])
# 手动调用predict
logits = model.predict(text_tokenized)
# 手动完成后处理
predict_label = logits[0].asnumpy().argmax()
result = label_map[predict_label]
print(result)  # 输出:"积极"

显然,自定义函数更符合 “用户友好” 的设计原则。

Model.predict 与自定义 predict 的关系

自定义 predict 函数依赖 Model.predict 完成模型推理,但在此基础上扩展了预处理和后处理逻辑。两者是 “基础功能” 与 “业务封装” 的关系,而非互斥关系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值