NNDL卷积神经网络-使用预训练resnet18实现CIFAR-10分类 [HBU]
目录
前言:
原文章:飞桨AI Studio星河社区-人工智能学习与实训社区 (baidu.com)
在本实践中,我们实践一个更通用的图像分类任务。
图像分类(Image Classification)是计算机视觉中的一个基础任务,将图像的语义将不同图像划分到不同类别。很多任务也可以转换为图像分类任务。比如人脸检测就是判断一个区域内是否有人脸,可以看作一个二分类的图像分类任务。
这里,我们使用的计算机视觉领域的经典数据集:
CIFAR-10数据集,网络为ResNet18模型,损失函数为交叉熵损失,优化器为Adam优化器,评价指标为准确率。
Adam优化器引入
Adam优化器的介绍参考《神经网络与深度学习》第7.2.4.3节。
参考文章:??简单认识Adam优化器 - 知乎 (zhihu.com)
之前的实验(FNN,CNN等),我们使用优化器都是SGD优化器,本次实验的优化器将选择Adam。基于随机梯度下降(SGD)的优化算法在科研和工程的很多领域里都是极其核心的。很多理论或工程问题都可以转化为对目标函数进行最小化的数学问题。
按吴恩达老师所说的,梯度下降(Gradient Descent)就好比一个人想从高山上奔跑到山谷最低点,用最快的方式(steepest)奔向最低的位置(minimum)。
2014年12月, Kingma和Lei Ba两位学者提出了Adam优化器,结合AdaGrad和RMSProp两种优化算法的优点。对梯度的一阶矩估计(First Moment Estimation,即梯度的均值)和二阶矩估计(Second Moment Estimation,即梯度的未中心化的方差)进行综合考虑,计算出更新步长。
Adam主要包含以下几个显著的优点:
- 实现简单,计算高效,对内存需求少
- 参数的更新不受梯度的伸缩变换影响
- 超参数具有很好的解释性,且通常无需调整或仅需很少的微调
- 更新的步长能够被限制在大致的范围内(初始学习率)
- 能自然地实现步长退火过程(自动调整学习率)
- 很适合应用于大规模的数据及参数的场景
- 适用于不稳定目标函数
- 适用于梯度稀疏或梯度存在很大噪声的问题
综合Adam在很多情况下算作默认工作性能比较优秀的优化器。
数据处理
CIFAR-10数据集包含了10种不同的类别、共60,000张图像,其中每个类别的图像都是6000张,图像大小均为32×32像素--RGB图像(彩色图像)。
CIFAR10数据集用来监督学习训练,每个样本都配备一个标签值,CIFAR10中有10类物体,标签值分别按照0~9来区分,分别是飞机( airplane )、汽车( automobile )、鸟( bird )、猫( cat )、鹿( deer )、狗( dog )、青蛙( frog )、马( horse )、船(ship )、卡车( truck )。
CIFAR-10数据集的示例如下:
*将文件数据集进行解压
(因为我的电脑没有安装解压文件,一直都是使用电脑自带的方法去解压文件,今天在这里记录一下解压步骤,参考文章:Windows下解压tar.gz压缩文件_tar.gawindows怎么用-CSDN博客)
在windows上搜索,并打开 Windows PowerShell
然后将下载好的数据集文件拖放到桌面上,复制数据集文件的名称。然后在Windows PowerShell上输入以下两个语句:
>cd desktop
>tar -zxvf 文件名
然后在桌面上就多出来解压成功的文件:
数据读取
在本实验中,将原始训练集拆分成了train_set、dev_set两个部分,分别包括40 000条和10 000条样本。将data_batch_1到data_batch_4作为训练集,data_batch_5作为验证集,test_batch作为测试集。 最终的数据集构成为:
- 训练集:40 000条样本。
- 验证集:10 000条样本。
- 测试集:10 000条样本。
读取一个batch数据的代码和流程如下所示:
#数据集读取
import os
import pickle
import numpy as np
#folder_path:数据集文件所在的文件夹路径
#batch_id:要加载的批次编号,默认为1
#mode:指定加载模式,默认为train
def load_cifar10_batch(folder_path, batch_id=1, mode='train'):
#根据mode参数的值,确定要加载的文件路径
if mode == 'test':
file_path = os.path.join(folder_path, 'test_batch')
else:
file_path = os.path.join(folder_path, 'data_batch_'+str(batch_id))
#加载数据集文件
with open(file_path, 'rb') as batch_file:
#使用pickle.load加载pickle文件将数据集批量加载到内存中
batch = pickle.load(batch_file, encoding = 'latin1')
#使用reshape将图像变为(图像数量,3,32,32)的三位形状,因为原始图像的数据是扁平的。
#同时将像素值除以255进行归一化,是其范围在[0,1]之间
imgs = batch['data'].reshape((len(batch['data']),3,32,32)) / 255.
labels = batch['labels']
return np.array(imgs, dtype='float32'), np.array(labels)
imgs_batch, labels_batch = load_cifar10_batch(folder_path=r'C:\Users\27513\Desktop\FAN\NNDL\cifar-10-batches-py',
batch_id=1, mode='train')
查看数据中的维度:
#打印一下每个batch中X和y的维度
print ("batch of imgs shape: ",imgs_batch.shape, "batch of labels shape: ", labels_batch.shape)
结果为:
可视化观察其中的一张样本图像和对应的标签:
#可视化观察其中的一张样本图像和对应的标签
import matplotlib.pyplot as plt
image, label = imgs_batch[1], labels_batch[1]
print("The label in the picture is {}".format(label))
plt.figure(figsize=(2, 2))
plt.imshow(image.transpose(1,2,0))
plt.savefig('cnn-car.pdf')
可以多查看几张照片以及对应的标签,结果为:
构造Dataset类
构造一个CIFAR10Dataset类,流程及代码如下:
#构造Dadaset类
import torch
from torch.utils.data import Dataset,DataLoader
import torchvision.transforms as transforms
#定义CIFAR10Dataset类,继承自Dataset类
class CIFAR10Dataset(Dataset):
def __init__(self, folder_path=r'C:\\Users\\27513\\Desktop\\FAN\\NNDL\\cifar-10-batches-py', mode='train'):
if mode == 'train':
#加载batch1-batch4作为训练集作为训练数据,将每个batch的图像和标签数据连接起来
self.imgs, self.labels = load_cifar10_batch(folder_path=folder_path, batch_id=1, mode='train')
for i in range(2, 5):
imgs_batch, labels_batch = load_cifar10_batch(folder_path=folder_path, batch_id=i, mode='train')
self.imgs, self.labels = np.concatenate([self.imgs, imgs_batch]), np.concatenate([self.labels, labels_batch])
elif mode == 'dev':
#加载batch5作为验证集
self.imgs, self.labels = load_cifar10_batch(folder_path=folder_path, batch_id=5, mode='dev')
elif mode == 'test':
#加载测试集
self.imgs, self.labels = load_cifar10_batch(folder_path=folder_path, mode='test')
#调整图像大小为32*32,转换为张量,并使用给定的均值和标准差进行归一化
self.transforms = transforms.Compose([transforms.Resize(32),transforms.ToTensor(), transforms.Normalize(mean=[0.4914,0.4822,0.4465], std=[0.2023, 0.1994, 0.2010])])
#通过索引获取数据集中的项目
def __getitem__(self, idx):
img, label = self.imgs[idx], self.labels[idx]
img = self.transform(img)
return img, label #返回一个图像和对应的标签
#返回数据集中项目的数量
def __len__(self):
return len(self.imgs)
#实例化训练、验证和测试数据集对象
train_dataset = CIFAR10Dataset(folder_path=r'C:\\Users\\27513\\Desktop\\FAN\\NNDL\\cifar-10-batches-py', mode='train')
dev_dataset = CIFAR10Dataset(folder_path=r'C:\\Users\\27513\\Desktop\\FAN\\NNDL\\cifar-10-batches-py', mode='dev')
test_dataset = CIFAR10Dataset(folder_path=r'C:\\Users\\27513\\Desktop\\FAN\\NNDL\\cifar-10-batches-py', mode='test')
模型构建
使用PyTorch API中的Resnet18进行图像分类实验:
#模型构建
from torchvision.models import resnet18
resnet18_model = resnet18(weights = True)
模型训练
复用RunnerV3类,实例化RunnerV3类,并传入训练配置。
使用训练集和验证集进行模型训练,共训练30个epoch。
在实验中,保存准确率最高的模型作为最佳模型。代码实现如下:
import torch.nn.functional as F
import torch.optim as opt
from nndl import RunnerV3, Accuracy
# 学习率大小
lr = 0.001
# 批次大小
batch_size = 64
# 加载数据
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
dev_loader = DataLoader(dev_dataset, batch_size=batch_size)
test_loader = DataLoader(test_dataset, batch_size=batch_size)
# 定义网络
model = resnet18_model.to(device)
# 定义优化器,这里使用Adam优化器以及l2正则化策略,相关内容在7.3.3.2和7.6.2中会进行详细介绍
optimizer = opt.Adam(lr=lr,params=model.parameters(), weight_decay=0.005)
# 定义损失函数
loss_fn = F.cross_entropy
loss_fn = loss_fn
# 定义评价指标
metric = Accuracy(is_logist=True)
# 实例化RunnerV3
runner = RunnerV3(model, optimizer, loss_fn, metric)
# 启动训练
log_steps = 3000
eval_steps = 3000
runner.train(train_loader, dev_loader, num_epochs=30, log_steps=log_steps,
eval_steps=eval_steps, save_path="best_model.pdparams")
注:使用到的RunnerV3和Accuracy如下
RunnerV3,记得添加到nndl.runner文件包中:
class RunnerV3(object):
def __init__(self, model, optimizer, loss_fn, metric, **kwargs):
self.model = model
self.optimizer = optimizer
self.loss_fn = loss_fn
self.metric = metric # 只用于计算评价指标
# 记录训练过程中的评价指标变化情况
self.dev_scores = []
# 记录训练过程中的损失函数变化情况
self.train_epoch_losses = [] # 一个epoch记录一次loss
self.train_step_losses = [] # 一个step记录一次loss
self.dev_losses = []
# 记录全局最优指标
self.best_score = 0
def train(self, train_loader, dev_loader=None, **kwargs):
# 将模型切换为训练模式
self.model.train()
# 传入训练轮数,如果没有传入值则默认为0
num_epochs = kwargs.get("num_epochs", 0)
# 传入log打印频率,如果没有传入值则默认为100
log_steps = kwargs.get("log_steps", 100)
# 评价频率
eval_steps = kwargs.get("eval_steps", 0)
# 传入模型保存路径,如果没有传入值则默认为"best_model.pdparams"
save_path = kwargs.get("save_path", "best_model.pdparams")
custom_print_log = kwargs.get("custom_print_log", None)
# 训练总的步数
num_training_steps = num_epochs * len(train_loader)
if eval_steps:
if self.metric is None:
raise RuntimeError('Error: Metric can not be None!')
if dev_loader is None:
raise RuntimeError('Error: dev_loader can not be None!')
# 运行的step数目
global_step = 0
# 进行num_epochs轮训练
for epoch in range(num_epochs):
# 用于统计训练集的损失
total_loss = 0
for step, data in enumerate(train_loader):
X, y = data
# 获取模型预测
logits = self.model(X.to(device))
loss = self.loss_fn(logits, y.long().to(device)) # 默认求mean
total_loss += loss
# 训练过程中,每个step的loss进行保存
self.train_step_losses.append((global_step, loss.item()))
if log_steps and global_step % log_steps == 0:
print(
f"[Train] epoch: {epoch}/{num_epochs}, step: {global_step}/{num_training_steps}, loss: {loss.item():.5f}")
# 梯度反向传播,计算每个参数的梯度值
loss.backward()
if custom_print_log:
custom_print_log(self)
# 小批量梯度下降进行参数更新
self.optimizer.step()
# 梯度归零
self.optimizer.zero_grad()
# 判断是否需要评价
if eval_steps > 0 and global_step > 0 and \
(global_step % eval_steps == 0 or global_step == (num_training_steps - 1)):
dev_score, dev_loss = self.evaluate(dev_loader, global_step=global_step)
print(f"[Evaluate] dev score: {dev_score:.5f}, dev loss: {dev_loss:.5f}")
# 将模型切换为训练模式
self.model.train()
# 如果当前指标为最优指标,保存该模型
if dev_score > self.best_score:
self.save_model(save_path)
print(
f"[Evaluate] best accuracy performence has been updated: {self.best_score:.5f} --> {dev_score:.5f}")
self.best_score = dev_score
global_step += 1
# 当前epoch 训练loss累计值
trn_loss = (total_loss / len(train_loader)).item()
# epoch粒度的训练loss保存
self.train_epoch_losses.append(trn_loss)
print("[Train] Training done!")
# 模型评估阶段,使用'torch.no_grad()'控制不计算和存储梯度
@torch.no_grad()
def evaluate(self, dev_loader, **kwargs):
assert self.metric is not None
# 将模型设置为评估模式
self.model.eval()
global_step = kwargs.get("global_step", -1)
# 用于统计训练集的损失
total_loss = 0
# 重置评价
self.metric.reset()
# 遍历验证集每个批次
for batch_id, data in enumerate(dev_loader):
X, y = data
# 计算模型输出
logits = self.model(X.to(device))
# 计算损失函数
loss = self.loss_fn(logits, y.long().to(device)).item()
# 累积损失
total_loss += loss
# 累积评价
self.metric.update(logits, y.to(device))
dev_loss = (total_loss / len(dev_loader))
dev_score = self.metric.accumulate()
# 记录验证集loss
if global_step != -1:
self.dev_losses.append((global_step, dev_loss))
self.dev_scores.append(dev_score)
return dev_score, dev_loss
# 模型评估阶段,使用'torch.no_grad()'控制不计算和存储梯度
@torch.no_grad()
def predict(self, x, **kwargs):
# 将模型设置为评估模式
self.model.eval()
# 运行模型前向计算,得到预测值
logits = self.model(x.to(device))
return logits
def save_model(self, save_path):
torch.save(self.model.state_dict(), save_path)
def load_model(self, model_path):
state_dict = torch.load(model_path)
self.model.load_state_dict(state_dict)
Accuracy:
class Accuracy():
def __init__(self, is_logist=True):
# 用于统计正确的样本个数
self.num_correct = 0
# 用于统计样本的总数
self.num_count = 0
self.is_logist = is_logist
def update(self, outputs, labels):
# 判断是二分类任务还是多分类任务,shape[1]=1时为二分类任务,shape[1]>1时为多分类任务
if outputs.shape[1] == 1: # 二分类
outputs = torch.squeeze(outputs, dim=-1)
if self.is_logist:
# logist判断是否大于0
preds = torch.tensor((outputs >= 0), dtype=torch.float32)
else:
# 如果不是logist,判断每个概率值是否大于0.5,当大于0.5时,类别为1,否则类别为0
preds = torch.tensor((outputs >= 0.5), dtype=torch.float32)
else:
# 多分类时,使用'torch.argmax'计算最大元素索引作为类别
preds = torch.argmax(outputs, dim=1)
# 获取本批数据中预测正确的样本个数
labels = torch.squeeze(labels, dim=-1)
batch_correct = torch.sum(torch.tensor(preds == labels, dtype=torch.float32)).cpu().numpy()
batch_count = len(labels)
# 更新num_correct 和 num_count
self.num_correct += batch_correct
self.num_count += batch_count
def accumulate(self):
# 使用累计的数据,计算总的指标
if self.num_count == 0:
return 0
return self.num_correct / self.num_count
def reset(self):
# 重置正确的数目和总数
self.num_correct = 0
self.num_count = 0
def name(self):
return "Accuracy"
运行结果:
可视化观察训练集与验证集的准确率及损失变化情况:
from nndl import plot
plot(runner, fig_name='cnn-loss4.pdf')
Plot代码如下
def plot(runner, fig_name):
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
train_items = runner.train_step_losses[::30]
train_steps = [x[0] for x in train_items]
train_losses = [x[1] for x in train_items]
plt.plot(train_steps, train_losses, color='#8E004D', label="Train loss")
if runner.dev_losses[0][0] != -1:
dev_steps = [x[0] for x in runner.dev_losses]
dev_losses = [x[1] for x in runner.dev_losses]
plt.plot(dev_steps, dev_losses, color='#E20079', linestyle='--', label="Dev loss")
# 绘制坐标轴和图例
plt.ylabel("loss", fontsize='x-large')
plt.xlabel("step", fontsize='x-large')
plt.legend(loc='upper right', fontsize='x-large')
plt.subplot(1, 2, 2)
# 绘制评价准确率变化曲线
if runner.dev_losses[0][0] != -1:
plt.plot(dev_steps, runner.dev_scores,
color='#E20079', linestyle="--", label="Dev accuracy")
else:
plt.plot(list(range(len(runner.dev_scores))), runner.dev_scores,
color='#E20079', linestyle="--", label="Dev accuracy")
# 绘制坐标轴和图例
plt.ylabel("score", fontsize='x-large')
plt.xlabel("step", fontsize='x-large')
plt.legend(loc='lower right', fontsize='x-large')
plt.savefig(fig_name)
plt.show()
结果显示,准确度最终达到了81.26%,训练集上的误差最终减小到0.14016,验证集上的误差为1.01485。
可视化图:
这个训练过程进行了大概七个小时?,模型太复杂,backward过程耗时太多。
?在本实验中,使用了第7章中介绍的Adam优化器进行网络优化,如果使用SGD优化器,会造成过拟合的现象,在验证集上无法得到很好的收敛效果。可以尝试使用第7章中其他优化策略调整训练配置,达到更高的模型精度。
模型评价
使用测试数据对在训练过程中保存的最佳模型进行评价,观察模型在测试集上的准确率以及损失情况。代码实现如下:
# 加载最优模型
runner.load_model('best_model.pdparams')
# 模型评价
score, loss = runner.evaluate(test_loader)
print("[Test] accuracy/loss: {:.4f}/{:.4f}".format(score, loss))
运行结果:
模型预测
同样地,也可以使用保存好的模型,对测试集中的数据进行模型预测,观察模型效果,具体代码实现如下:
#获取测试集中的一个batch的数据
for X, label in test_loader:
logits = runner.predict(X)
#多分类,使用softmax计算预测概率
pred = F.softmax(logits)
#获取概率最大的类别
pred_class = torch.argmax(pred[2]).numpy()
label = label[2].data.numpy()
#输出真实类别与预测类别
print("The true category is {} and the predicted category is {}".format(classes[label], classes[pred_class]))
#可视化图片
X=np.array(X)
X=X[1]
plt.imshow(X.transpose(1, 2, 0))
plt.show()
break
运行结果:
什么是“预训练模型”?什么是“迁移学习”?
预训练模型:
它的基本思想是先在一个大规模的数据集上训练一个模型,学习通用的特征表示,然后再在一个小规模的、特定任务的数据集上对模型进行微调,提高模型的性能。预训练模型可以解决标注数据稀缺和领域迁移的问题,提高模型的泛化能力和可扩展性。
举一个例子:让一个完全不懂英文的人去做英文法律文书的关键词提取的工作会完全无法进行,或者说ta需要非常多的时间去学习(因为ta现在根本看不懂英文)。但是如果让一个英语为母语但是没接触过此类工作的人去做这项任务,ta可能只需要相对比较短的时间学习就可以上手这项任务。在这里,英文知识就属于“共性”的知识,这类知识不必要只通过英文法律文书的相关语料进行学习,而是可以通过大量英文语料,不管是小说、书籍,还是自媒体,都可以是学习资料的来源。
迁移学习(Transfer Learning):
迁移学习之——什么是迁移学习(Transfer Learning) (zhihu.com)
在某些机器学习场景中,由于直接对目标域从头开始学习成本太高,因此我们期望运用已有的相关知识来辅助尽快地学习新知识。比如,已经会下中国象棋,就可以类比着来学习国际象棋;已经会编写Java程序,就可以类比着来学习C#;已经学会英语,就可以类比着来学习法语;已经学会了骑自行车,就可以类比学习骑摩托车;等等。正是通过两种事物之间的相似性,可以构建一种从旧知识到新知识的迁移桥梁,从而可以更快更好的学习新知识。
迁移学习(Transfer Learning)通俗来讲就是学会举一反三的能力,通过运用已有的知识来学习新的知识,其核心是找到已有知识和新知识之间的相似性,通过这种相似性的迁移达到迁移学习的目的。世间万事万物皆有共性,如何合理地找寻它们之间的相似性,进而利用这个桥梁来帮助学习新知识,是迁移学习的核心问题。
比较 '使用预训练模型' 和 '不使用预训练模型' 的效果
resnet = models.resnet18(pretrained=True)
resnet = models.resnet18(pretrained=False)
使用预训练模型已经在上文做了测试,接下来不使用预训练模型做测试:
只需要修改这一行就行:
resnet18_model = resnet18(pretrained=False)
可以看出不使用预训练模型的准确率和误差都不如使用预训练模型的效果。
预训练模型优点:
1、开源模型多,可以直接用于目标检测
2、可以快速地得到最终模型,需要的训练数据少
缺点:
1、预训练模型大、参数多、模型结构灵活性差、难以改变网络结构,计算量大,限制应用场景
2、分类和检测任务损失函数和类别分布不同,优化空间存在差异
3、尽管微调可以减少不同目标类别分布差异性,差异太大时,微调效果不明显
?
不使用预训练模型运行结果:
使用预训练模型的运行结果:
对比使用预训练模型和不使用预训练模型,可以直观得到结果:
1.使用预训练模型收敛速度更快,更稳定(从损失的可视化可以看出)。
2.使用预训练模型的准确率更高(从运行结果可以看出)。
总结
1. 在进行模型构建的时候,我参考了代码:
from torchvision.models import resnet18
resnet18_model = resnet18(pretrained=True)
出现了2条报错(警告)信息:
现在来解决警告信息:
针对第二条报错信息,解释意思为:收到的警告消息来自于PyTorch库,它告诉你关于ResNet18模型的一个即将废弃的参数使用方式。你正在使用一个即将被废弃的方式来指定模型权重,这种方式将在新版本的库中被移除。
解决方法就是将pretrained=True 改为weights = True。
2.在进行模型训练的时候,遇到了很多很多小问题,但是但凡只要有一点小问题,代码就运行不出来。发愁了一晚上+半个下午,修修改改,不知道如何解决RunnerV3这块代码的报错,报错类型一般为:
1.)找不到RunnerV3类。我根据报错提示的路径找到了nndl.runner的包中,将现成的RunnerV3类加入到nndl.runner文件中,于是就继续报错找不到device的定义。
2.)找不到device的定义,我就想自己尝试着写,添加对device的定义...这个想法实现起来太困难了,RunnerV3类都是企业大佬们写好的,我试着定义了几次device,结果就是越写越糟糕。于是放弃了
(当时没有截图,只想着怎么把这些个错误解决了。)
我去借鉴了上一届学长们的代码,每个版本运行起来都会出现新的问题,保研的学长定义了cuda进行训练,我也照着去调用自己的cuda,但是没有成功。 下图是学长的训练方法:
NNDL 实验六 卷积神经网络(5)使用预训练resnet18实现CIFAR-10分类_crack500数据集_笼子里的薛定谔的博客-CSDN博客
改代码改到了怀疑人生,无奈之下求助了舍友,让好舍友把她的代码发过来,小改了一些文件路径,终于可以运行了!(很心酸,最终还是没有自己成功调试代码),感谢我的好舍友们!
代码运行成功之后,就开始了漫漫训练之等待,第一次训练 运行了大概有两个半小时,后来居然报错了!!报错信息为:
无奈之下把这个错误改了之后重头跑了第二遍,又花了将近三个小时,才得到结果。
(实在不想跑第三遍了)
3. 这次的实验仍然是使用原来实验的'老套路',大体流程为:数据处理(数据读取)-模型构建(搭建网络)-模型训练(优化器、评价指标等)-模型评价-模型预测,再生成几样可视化图,进行结果分析。
? ? ? ? 本次实验的难点在于数据集体量太大,一般的笔记本电脑带不动,我在模型训练的时候仅仅进行了15轮,15轮的一次训练就花费了将近三个小时,而且并不是一次就能得到正确结果。跑完第一遍之后,得到了错误信息,改正之后还要重新运行第二遍。一晃过去又是三个小时,这对于多次调试参数是非常不利的。
本博客中使用到的优秀内容链接如下,在此鸣谢:
Windows下解压tar.gz压缩文件_tar.gawindows怎么用-CSDN博客
【神经网络与深度学习】CIFAR10数据集介绍,并使用卷积神经网络训练图像分类模型——[附完整训练代码]_路遥_.的博客-CSDN博客
HBU-NNDL 实验六 卷积神经网络(5)使用预训练resnet18实现CIFAR-10分类_from torchvision.models import resnet18-CSDN博客
NNDL 实验六 卷积神经网络(5)使用预训练resnet18实现CIFAR-10分类_crack500数据集_笼子里的薛定谔的博客-CSDN博客
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!