目录
为什么 “函数输出多项时,微分函数会求所有输出项对参数的导数”?
为什么 “用 stop_gradient 可以屏蔽 z 对梯度的影响”?
求得w、b对应的梯度值与初始function求得的梯度值一致,同时z能够作为微分函数的输出返回。怎么理解?
model是mindspore的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 完成模型推理,但在此基础上扩展了预处理和后处理逻辑。两者是 “基础功能” 与 “业务封装” 的关系,而非互斥关系。