pytorch项目实战-分类模型李宏毅 21 机器学习第三次作业代码详解 CNN图片分类任务
深度学习领域中,卷积神经网络(CNN)是一个绕不开的重要主题。本章节旨在深入探讨和实践CNN相关的概念和知识点,将依据李宏毅教授在2021年机器学习课程中的第三次作业代码作为例子。通过这一实例,读者将有机会加深对CNN的理解,并提升自己的实践技能。需要指出的是,本章节中展示的代码并未直接采用课程提供的baseline示例。这是因为基础模型的性能有限,而且与传统的深度神经网络(DNN)代码相比,并不

CNN 卷积神经网络食物分类任务
前言
深度学习领域中,卷积神经网络(CNN)是一个绕不开的重要主题。本章节旨在深入探讨和实践CNN相关的概念和知识点,将依据李宏毅教授在2021年机器学习课程中的第三次作业代码作为例子。通过这一实例,读者将有机会加深对CNN的理解,并提升自己的实践技能。
需要指出的是,本章节中展示的代码并未直接采用课程提供的baseline示例。这是因为基础模型的性能有限,而且与传统的深度神经网络(DNN)代码相比,并不提供更多的学习内容。因此,自行选择了一套演示代码,旨在提供更深入的学习和探索机会。对于有兴趣进一步研究的读者,推荐参考原始代码。
通过本章节的学习,希望帮助读者不仅掌握卷积神经网络的理论基础,还能通过亲手实践,有效地加深对这一强大工具的理解和应用能力。
一、数据集介绍

- 食物分类数据集
- 图像来自被分为11类的food-11数据集。
- 这里的数据集有所修改:
- 训练集:280*11张有标签的图像 + 6786张无标签的图像
- 验证集:60*11张有标签的图像
- 测试集:3347张图像
因为测试部分需要使用kaggle进行上传测试故此,本章节不会对测试代码进行讲解,具体感兴趣的同学可以移步Kaggle进行上传测试
为了规范化地访问数据,数据集被组织如下所示:同一类别的图片被放置在相同的文件夹内,同时测试集和验证集的图片也已被完整地分配到不同的文件夹中以便于调用:

二、CNN模型整体框架
借助之前章节的学习,读者对深度学习代码的整体框架已有充分的了解。因此,本章节的框架结构图仅对重要部分进行了简明划分。在随后的代码详解部分,将针对各个部分进行深入讲解。框架的整体布局是按照编码的逻辑顺序排列的。如果读者在详细分析各个部分时对整个流程感到困惑,可以回顾本节,以便更好地理解代码的整体逻辑。

三、卷积神经网络代码详解
3.1 导入需要使用的包
先来看下模型所需要的使用的库函数,本章节对之前文章中未出现的库函数进行讲解。
# 常规的库函数
import numpy as np
import torch
import torch.nn as nn
#针对当前部分引入新的库函数
import torchvision
import torchvision.transforms as transforms
from PIL import Image
# "ConcatDataset" and "Subset" are possibly useful when doing semi-supervised learning.
from torch.utils.data import ConcatDataset, DataLoader, Subset, Dataset
from torchvision.datasets import DatasetFolder
# 进度条
from tqdm.auto import tqdm
本章节出现了新的库函数如下:
import torchvision
torchvision独立于pytorch,专门用来处理图像,通常用于计算机视觉领域。
import torchvision.transforms as transforms
transforms 是一个函数,主要作用就是对用于图像预处理和数据增强。transforms 通常与 transforms.Compose 配合使用。其主要的子方法基本就是对图片处理,其还提供Compose方法将多种对图片的处理方式进行融合,变成一种形式。t
举个例子:
from torchvision import transforms
# 定义图像预处理操作
transform = transforms.Compose([
transforms.Resize(256), # 缩放图像,使较短的边为256像素
transforms.CenterCrop(224), # 从图像中心裁剪出224*224大小的图像
transforms.ToTensor(), # 将图像转换为Tensor,归一化至[0,1]
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # 标准化处理
])
# 然后你可以使用这个 `transform` 来处理你的图像数据
# 例如:
# image = Image.open("path_to_your_image.jpg") 当前步骤实例化一个图像的对象,送入transform进行处理
# image = transform(image)
from PIL import Image # 打开图片对图进行初步处理
主要的作用Image模块是Pillow库的核心模块,用于图像的加载、处理和保存。通过Image模块的对Image.open打开文件,实例化图片,通过对象的方法实现对图像的改变。
from PIL import Image
# 加载图像文件
image = Image.open("example.jpg")
# 显示图像
image.show()
# 对图像进行一些处理
# 比如旋转90度
rotated_image = image.rotate(90)
# 或者将图像转换为灰度
greyscale_image = image.convert("L")
# 又或者调整图像的大小
resized_image = image.resize((300, 300))
from torch.utils.data import ConcatDataset, DataLoader, Subset, Dataset
ConcatDataset 是一个 PyTorch 数据集类,它用于组合多个具有相同的字段(如图像和标签)的数据集。
dataset1 = CustomDataset('path/to/dataset1')
dataset2 = CustomDataset('path/to/dataset2')
# Concatenate datasets
combined_dataset = ConcatDataset([dataset1, dataset2])
Subset 类用于从一个大数据集中选取特定的子集。
# Assuming dataset is a large dataset and you want to select a subset of it
subset_indices = [10, 20, 30, 40, 50] # Just as an example
subset = Subset(dataset, subset_indices)
from torchvision.datasets import DatasetFolder
torchvision.datasets.DatasetFolder 是 PyTorch torchvision 库中一个非常实用的类,它允许快速且轻松地按照特定的文件结构组织和加载数据集,针对那些已经以一定格式存放的数据集特别有用。 这种方式可以大大减少手动编写用于加载数据集的代码量,尤其是当数据集已经按照一定的目录结构组织时。
具体的格式如下:
一目了然,就是将相同类别的数据存储到一个文件夹中,这样就能够使用该类进行数据集好的快速创建。
3.2 数据集,数据加载器,数据增强操作
3.2.1 数据增强
在深度学习特别是图像处理领域中,数据增强(Data Augmentation)是一个非常关键的技术,它可以增加数据集的多样性,减少模型对特定形式输入数据的依赖,从而提高模型的泛化能力。
通过应用各种随机变换,数据增强能够从现有数据集生成新的训练样本,这些变换包括但不限于旋转、平移、缩放、裁剪、翻转、调整亮度/对比度等。transforms 模块提供了许多此类操作,可以方便地用于图像数据的增强。
# It is important to do data augmentation in training.
train_tfm = transforms.Compose([
transforms.RandomResizedCrop((128, 128)),
# 随机从图中裁切一部分,并将裁减的部分的大小限定在128,128。增加随机行模型能够得到更多的训练样本
transforms.RandomChoice( # 在下面的几种方式中随机选择一种进行
'''
简单点说就是在不同的数据集上得到的数据增强的技术:
1. **`transforms.AutoAugment()`:使用默认的 AutoAugment 策略。如果不指定任何策略
2. ,那么将使用 ImageNet 数据集上实验确定的最佳增强策略。这是一种通用的增强策略,适用于多种图像分类任务。
3. **`transforms.AutoAugment(transforms.AutoAugmentPolicy.CIFAR10)`**:
4. 使用为 CIFAR10 数据集特别设计的 AutoAugment 策略。CIFAR10 包含10个类别的小尺
5. 寸彩色图像(32x32像素)。为这个数据集定制的策略考虑到了其特定的特性,如图像尺寸和类型的多样性。
6. **`transforms.AutoAugment(transforms.AutoAugmentPolicy.SVHN)`**:使用为
7. 街景门牌号(SVHN, Street View House Numbers)数据集特别设计的 AutoAugment 策略
8. 。SVHN 是现实世界的数字识别数据集,图像来源于谷歌街景的门牌号。因此,这项策略是针对识别数字类别的图像进行优化的。
'''
[transforms.AutoAugment(),
transforms.AutoAugment(transforms.AutoAugmentPolicy.CIFAR10),
transforms.AutoAugment(transforms.AutoAugmentPolicy.SVHN)]
),
transforms.RandomHorizontalFlip(p=0.5), #以 50% 的概率水平翻转图像。这种随机性增加了训练数据的多样性。
transforms.ColorJitter(brightness=0.5), #随机改变图像亮度,亮度参数 brightness=0.5 提供了亮度变化的强度。
transforms.RandomAffine(degrees=20, translate=(0.2, 0.2), scale=(0.7, 1.3)),
# 应用随机仿射变换:
# degrees=20 表示随机旋转的角度范围为 [-20, 20] 度。
# translate=(0.2, 0.2) 允许平移至多达图像宽度和高度20%的大小。
# scale=(0.7, 1.3) 指定缩放比例范围,在原始大小的基础上进行放大或缩小。
transforms.ToTensor(), #将 PIL 图像 FloatTensor,并将图像的像素值从 [0, 255] 范围缩放到 [0.0, 1.0] 范围。
])
# 并不需要对测试和验证集的数据进行增强
# 需要将所有的图片的大小进行设定,并且转换成tensor类型
test_tfm = transforms.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
])
通过各种各样的手段对数据进行处理,从而在有限的数据集上能够提供更多的样本给模型进行训练,从而提升模型的性能。
3.2.2 数据集构建
DatasetFolder("food-11/training/labeled", loader=lambda x: Image.open(x), extensions="jpg", transform=train_tfm)
当前主要是针对DatasetFolder这个类实现进行实例化,创建数据集,前提是按照上文中描述的数据集的格式是按照DatasetFolder类的要求进行创建的。
举个例子解释一下,要创建数据集首先就要知道数据集的位置,故此第一个参数是数据集的根目录,然后就是读取数据文件,使用Image.open(x)方法进行打开,而loader作为加载器是会接收到位置信息的,故此这个匿名函数lambda就可以正常运行,为了防止访问到其他非图片数据因此要限定访问数据的格式,在得到了数据对象后,应用上文讲解的数据增强技术对数据进行处理,最终得到数据集对象。由此也可以看到torchvision.Composs和DataFolder的配套使用的。
这行代码使用PyTorch的DatasetFolder来加载一个具有标记的训练数据集,其中包含了对数据进行加载和预处理的具体设置。下面是对其各个参数的解释:
-
路径(“food-11/training/labeled”):这是数据集的根目录路径,
DatasetFolder会递归遍历这个目录下的所有子目录来查找符合指定扩展名的文件。在这个例子中,"food-11/training/labeled"目录下应该包含了不同类别的食物图片,每个类别的图片存放在以类别命名的子目录中。 -
loader(
lambda x: Image.open(x)):这是一个函数,用于指定如何加载一个图片文件。这里使用了一个lambda函数,它接收一个文件路径x作为输入,然后用PIL(Python Imaging Library,现在称为Pillow)的Image.open方法来打开这个图片文件。这样做的目的是使得每个图片文件被读入为一个PIL图像对象,方便后续进行图像处理。 -
extensions(“jpg”):这个参数用于指定哪些文件扩展名的文件会被视作数据集的一部分。在这个例子中,只有扩展名为"jpg"的文件会被加载。
-
transform(
train_tfm):这个参数接收一个torchvision.transforms操作的组合,用于对每个加载的图片进行预处理。train_tfm可能是用transforms.Compose创建的多个图像变换操作的组合,例如缩放、裁剪、归一化等。这些变换操作能够自动应用于每个加载的图片上,常用于数据增强和预处理以适配神经网络模型的输入需求。
train_set = DatasetFolder("food-11/training/labeled", loader=lambda x: Image.open(x), extensions="jpg", transform=train_tfm)
valid_set = DatasetFolder("food-11/validation", loader=lambda x: Image.open(x), extensions="jpg", transform=test_tfm)
unlabeled_set = DatasetFolder("food-11/training/unlabeled", loader=lambda x: Image.open(x), extensions="jpg", transform=train_tfm)
test_set = DatasetFolder("food-11/testing", loader=lambda x: Image.open(x), extensions="jpg", transform=test_tfm)
3.2.3 加载器构建
# Construct data loaders.
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=2, pin_memory=True)
valid_loader = DataLoader(valid_set, batch_size=batch_size, shuffle=True, num_workers=2, pin_memory=True)
test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False)
# 和前文一样故此不在过多赘述,值得注意就是测试集不打乱数据
3.3 卷积神经网络构建
class Classifier(nn.Module):
def __init__(self):
super(Classifier, self).__init__()
# torch.nn.Conv2d(in_channels输入通道个数, out_channels输出通道个数, kernel_size卷积核大小, stride步长, padding是否填充)
# torch.nn.MaxPool2d(kernel_size通道数, stride步长, padding填充)
# input image size: [3, 128, 128]
self.cnn_layers = nn.Sequential(
nn.Conv2d(3, 64, 3, 1), # 卷积层
nn.BatchNorm2d(64), # 批归一化
nn.ReLU(),# 激活
nn.MaxPool2d(2, 2, 0),# 池化层
nn.Conv2d(64, 128, 3, 1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0),
nn.Conv2d(128, 256, 3, 1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0),
nn.Conv2d(256, 512, 3, 1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0),
nn.Conv2d(512, 1024, 3, 1),
nn.BatchNorm2d(1024),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0)
)
self.fc_layers = nn.Sequential(
nn.Linear(4096, 1024),
nn.BatchNorm1d(1024),
nn.ReLU(),
nn.Dropout(0.6),
nn.Linear(1024, 256),
nn.BatchNorm1d(256),
nn.ReLU(),
nn.Dropout(0.4),
nn.Linear(256, 11)
)
def forward(self, x):
# input (x): [batch_size, 3, 128, 128]
# output: [batch_size, 11]
x = self.cnn_layers(x)
x = x.flatten(1)
x = self.fc_layers(x)
return x
3.4 训练代码
3.4.1 半监督训练
简单的说,半监督训练本质上就是通过训练到一定程度的模型,对无标签数据进行标注,按照置信度判断是否作为训练数据,实现对训练数据的补充,从而依托更多的训练数据期望模型的性能上升。
在半监督训练框架下,模型首先使用少量的标记数据进行训练。一旦模型达到一定的性能水平,它就可以对未标记的数据进行预测,并给出每个样本的标签及其对应的置信度。然后,根据预先设定的置信度阈值,选择置信度高的预测结果将这些未标记的数据(现在带有预测标签)作为新的训练样本。通过这种方式,模型可以利用更多的数据,包括原始的标记数据和添加的预测标记数据,进行进一步的训练。这个循环可以按需重复进行,每一轮都可能提升模型对数据的理解和预测能力。
在实践中,这种方法的有效性很大程度上依赖于模型的初始性能以及如何选择置信度阈值来决定哪些未标记的样本被认为是可靠的训练数据。如果模型的初始预测准确性很高,那么利用未标记数据所带来的性能提升可能会更明显。相反,如果模型的初始预测性能较差,错误地标记大量未标记的数据可能会引起错误传播,进而影响模型的整体性能。
此外,半监督训练能够有效地缓解标记数据不足的问题,这在许多实际应用中是非常有价值的,因为手动标记大量数据往往是昂贵和耗时的。通过合理地利用未标记数据,半监督学习为学习更丰富和更泛化的模型提供了一条可行的路径。
代码部分
class PseudoDataset(Dataset): # 应用Dataset()类,构建数据集,可以通过,实例化过程中使用的都是模型预测的结果作为y,x就是图数据
def __init__(self, x, y):
self.x = x
self.y = y
def __getitem__(self, id): # 构建索引和数据之间的联系
return self.x[id][0], self.y[id]
def __len__(self):
return len(self.y)
def get_pseudo_labels(dataset, model, threshold=0.9): # 当前函数调用PseudoDataset生成无标签的数据集
device = "cuda" if torch.cuda.is_available() else "cpu"
data_loader = DataLoader(dataset, batch_size=batch_size, shuffle=False) # 导入无标签的数据集生成数据加载器
model.eval() # 模型开始评估模式
softmax = nn.Softmax(dim=-1)
'''
"torch的交叉熵损失函数本身就是具备softmax功能" 在使用交叉熵损失计算梯度时不需要再单独加 softmax 层,
但在预测类别时需要使用 softmax 来获取概率分布。
'''
idx = [] # 存放数据的索引,用于取出来数据
labels = [] # 预测的结果作为标签
for i, batch in enumerate(data_loader): # 数据分批导入
img, _ = batch # 当前部分的全部类别都是00,有疑问可以看下数据集
with torch.no_grad(): # 不会计算梯度
logits = model(img.to(device)) # 送入模型预测
probs = softmax(logits) #接入softmax层
for j, x in enumerate(probs): # 对所有的预测结果进行编码,j是序号而x是预测结果
if torch.max(x) > threshold: #比较向量中最大的值是否大于置信度
idx.append(i * batch_size + j) # 这个就是索引信息,batchsize就是批次个数乘i就会得到每一个批次的第一个数值的索引编号加上j就是真实的索引信息。
labels.append(int(torch.argmax(x))) # 这个就是类别最大的索引信息,x是一个向量取最大位置数值的索引作为真实标签
model.train() # 模型开始训练模式
print ("\nNew data: {:5d}\n".format(len(idx)))
dataset = PseudoDataset(Subset(dataset, idx), labels) # Subset 是按照idx提供的索引列表,从dataset中选出部分数据形成新的数据集。
return dataset
上述部分可以是本节最重要的内容,其他的其实都和之前的网络无太大差异
3.4.2 模型训练验证代码
这一部分的流程遵循了传统的模型训练与验证步骤,其核心目标是通过迭代更新参数以优化模型性能。在此过程中,模型首先在训练集上学习,然后通过验证集检验其泛化能力。不同的是,在这一阶段我们考虑了一种扩展策略:即是否引入半监督学习,也就是利用未标记数据来进一步增强模型的性能。
在这个上文中,半监督训练的实施为模型训练带来了新的维度。通过利用之前生成的带有伪标签的数据集,不仅使用了原始的有标记数据,还加入了额外的未标记数据,这些数据被模型预测并选择出高置信度的部分作为新的训练样本。这样的策略有助于模型从更多样化的数据中学习,尤其是在可用的标记数据较少时,这种方法尤为有价值。最终,半监督训练旨在通过扩大训练数据集的规模,提升模型对未见样本的识别能力。
通过这种灵活的训练策略,可以充分利用可用数据资源,进而朝着更加准确和鲁棒的模型性能迈进。综合使用传统训练方法和半监督训练策略,可为模型优化提供更全面的支持,从而在多个层面上提升模型性能。
device = "cuda" if torch.cuda.is_available() else "cpu"
# Initialize a model, and put it on the device specified.
model = Classifier().to(device) # 实例化模型送入设备上
model.device = device
from torchsummary import summary
summary(model, input_size=(3, 128, 128), device=device)
#通过提供模型实例和输入数据的尺寸,summary 函数允许你快速了解模型的构造和大小
#这在设计、调整网络结构时非常有用。
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.0003, weight_decay=1e-5)
# optimizer = torch.optim.SGD(model.parameters(), lr=0.0003, momentum=0.9, weight_decay=1e-5)
n_epochs = 500
do_semi = True # 是否使用半监督
model_path = "model.ckpt"
best_acc = 0.0
train_loss_record = []
valid_loss_record = []
train_acc_record = []
valid_acc_record = []
for epoch in range(n_epochs):
if do_semi and best_acc > 0.7 and epoch % 5 == 0: # 使用半监督进行增强训练的条件
pseudo_set = get_pseudo_labels(unlabeled_set, model) # 调用半监督函数 创建无标签数据集
concat_dataset = ConcatDataset([train_set, pseudo_set]) # 将两个数据集进行拼接
train_loader = DataLoader(concat_dataset, batch_size=batch_size, shuffle=True, num_workers=2, pin_memory=True, drop_last=True) # 得到新的数据集后构建增强数据集的加载器(原始训练集合加上无标签数据集)
# ---------- Train ----------
model.train()
train_loss = []
train_accs = []
# for batch in tqdm(train_loader):
for batch in train_loader:
imgs, labels = batch
logits = model(imgs.to(device)) #模型预测
loss = criterion(logits, labels.to(device)) #计算损失
optimizer.zero_grad() #梯度清0
loss.backward()# 计算梯度信息
grad_norm = nn.utils.clip_grad_norm_(model.parameters(), max_norm=10)
'''
可以被视作一种梯度归一化的技术。它确保模型中所有参数的梯度范数不超过一个特定
的最大阈值。如果梯度范数已经超过了这个阈值,那么会将梯度按比例缩小,以使得整
体梯度的范数恰好等于最大阈值,这个过程也被称为梯度裁剪。通过这种方式,它可以
防止在训练期间出现梯度爆炸问题,有助于保持训练过程的稳定性。
在某些情况下,当模型极度复杂或是训练数据包含异常值时,梯度可能会变得极大,
导致模型参数的大幅更新,从而影响模型的学习效果。通过梯度裁剪,可以限制梯度
更新的步幅,使得每次参数更新更加温和,从而有助于稳定训练过程,提高模型的训
练效率和最终性能。简而言之,梯度裁剪通过对梯度的大小进行限制,以实现对模型
训练过程的一种归一化控制,帮助防止梯度过大导致的训练不稳定问题。
'''
optimizer.step() #按学习率更新
acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()
# 本身就是一个二维的数据,因此要在最后一个维度找到最大值并取出索引(logits.argmax(dim=-1) ,和真实标签比对,计算均值作为准确率
train_loss.append(loss.item())
train_accs.append(acc)
train_loss = sum(train_loss) / len(train_loss)
train_acc = sum(train_accs) / len(train_accs)
print(f"[ Train | {epoch + 1:03d} / {n_epochs:03d} ] loss = {train_loss:.5f}, acc = {train_acc:.5f}")
# ---------- Validation ---------- 验证部分
model.eval()
valid_loss = []
valid_accs = []
# for batch in tqdm(valid_loader):
for batch in valid_loader:
imgs, labels = batch
with torch.no_grad():
logits = model(imgs.to(device))
loss = criterion(logits, labels.to(device))
acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()
valid_loss.append(loss.item())
valid_accs.append(acc)
valid_loss = sum(valid_loss) / len(valid_loss)
valid_acc = sum(valid_accs) / len(valid_accs)
print(f"[ Valid | {epoch + 1:03d} / {n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")
# ---------- Record ----------
if valid_acc > best_acc: # 在最好的准确率下保存模型用于测试
best_acc = valid_acc
torch.save(model.state_dict(), model_path)
train_loss_record.append(train_loss)
valid_loss_record.append(valid_loss)
train_acc_record.append(train_acc)
valid_acc_record.append(valid_acc)
3.5 训练结果可视化
import matplotlib.pyplot as plt # 导入matplotlib.pyplot模块用于绘图
x = np.arange(len(train_acc_record)) # 创建一个与训练准确率记录长度相同的序列作为横坐标
plt.plot(x, train_acc_record, color="blue", label="Train") # 以蓝色线条绘制训练准确率变化趋势,添加标签"Train"
plt.plot(x, valid_acc_record, color="red", label="Valid") # 以红色线条绘制验证准确率变化趋势,添加标签"Valid"
plt.legend(loc="upper right") # 显示图例,并将其放置在图的右上角
plt.show() # 显示图像
3.6 训练结果可视化
测试阶段代码,将结果保存一个文档。
model.eval()
predictions = []
for batch in test_loader:
imgs, labels = batch
with torch.no_grad():
logits = model(imgs.to(device))
predictions.extend(logits.argmax(dim=-1).cpu().numpy().tolist())
with open("predict.csv", "w") as f: # 保存预测结果
f.write("Id,Category\n")
for i, pred in enumerate(predictions):
f.write(f"{i},{pred}\n")
总结
本章节深入浅出地探讨了使用PyTorch框架进行深度学习模型构建与训练的全流程,旨在帮助读者系统掌握从数据预处理、模型搭建、训练策略到验证评估的每一个环节。通过食物分类任务的实例,结合半监督学习这一高级训练策略,展示了提升模型性能的可能路径。这些内容的覆盖,旨在为读者搭建一个坚实的知识框架,让大家在深度学习的过程中稳健前行,还能灵活应对各种挑战。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)