Coggle 30 Days of ML 打卡任务三:苹果病害模型训练与预测

简介: Coggle 30 Days of ML 打卡任务三:苹果病害模型训练与预测

任务三:苹果病害模型训练与预测

  • 难度/分值:中/2

打卡内容

  1. 参赛选手名称:AppleDoctor
  2. 完成日期:2023.6.11
  3. 任务完成情况
  • 使用的编程语言:Python,PyTorch
  • 实现的功能:
  • 自定义数据集读取
  • 自定义CNN模型
  • 模型训练与验证
  • 对测试集进行预测

背景介绍

本次打卡任务是 Coggle 30 Days of ML 中的第三项任务。任务要求参赛选手利用提供的苹果病害数据集构建模型,并进行模型训练和预测。参赛选手可以选择合适的深度学习框架和模型架构,并使用训练集进行模型训练。然后,选手需要利用训练好的模型对测试集中的苹果叶片病害图像进行预测。

image.png

自定义数据集

首先,我们使用PyTorch创建自定义数据集,以适应我们的任务。这里我定义了一个名为AppleDataset的类。

# 定义Apple数据集的类
class AppleDataset(Dataset):
    def __init__(self, img_path, transform=None):
        """
        构造函数,初始化数据集的路径和数据增强操作
        Args:
            - img_path: list类型,存储数据集中图像的路径
            - transform: torchvision.transforms类型,数据增强操作
        """
        self.img_path = img_path
        self.transform = transform
    def __getitem__(self, index):
        """
        获取一个样本
        Args:
            - index: 数据集中的索引
        Returns:
            - img: Tensor类型,经过处理后的图像
            - label: Tensor类型,标签
        """
        img = Image.open(self.img_path[index])
        if self.transform is not None:
            img = self.transform(img)
        # 将类别名转换为数字标签
        class_name = self.img_path[index].split('/')[-2]
        if class_name in ['d1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9']:
            label = ['d1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9'].index(class_name)
        else:
            label = -1
        return img, torch.from_numpy(np.array(label))
    def __len__(self):
        """
        获取数据集的大小
        Returns:
            - size: int类型,数据集的大小
        """
        return len(self.img_path)

接下来,我们定义之前设置的数据增强方法,并应用于数据集。我们使用了以下数据增强操作:


调整图像大小:使用 transforms.Resize((224, 224)) 将图像大小调整为 224x224 像素。

随机水平翻转:使用 transforms.RandomHorizontalFlip() 以 0.5 的概率对图像进行水平翻转。

随机垂直翻转:使用 transforms.RandomVerticalFlip() 以 0.5 的概率对图像进行垂直翻转。

随机旋转:使用 transforms.RandomRotation(degrees=15) 以随机角度对图像进行旋转,角度范围为 -15 到 15 度。

转换为张量:使用 transforms.ToTensor() 将图像转换为张量。

归一化:使用 transforms.Normalize(mean, std) 对图像进行归一化处理,使用给定的均值和标准差。

# 定义图像预处理的参数
image_mean = [0.4940, 0.4187, 0.3855]
image_std = [0.2048, 0.1941, 0.1932]
# 定义训练集的数据增强操作
transform_train = transforms.Compose([
    transforms.Resize((224,224)), # 将图像大小调整为224*224
    transforms.RandomHorizontalFlip(), # 以0.5的概率对图像进行水平翻转
    transforms.RandomVerticalFlip(), # 以0.5的概率对图像进行垂直翻转
    transforms.RandomApply([transforms.GaussianBlur(kernel_size=3)], p=0.1), # 以0.1的概率对图像进行高斯模糊,卷积核大小为3
    transforms.RandomApply([transforms.ColorJitter(brightness=0.9)],p=0.5), # 以0.5的概率调整图像的亮度,亮度因子的范围为[0.1, 1.9]
    transforms.RandomApply([transforms.ColorJitter(contrast=0.9)],p=0.5), # 以0.5的概率调整图像的对比度,对比度因子的范围为[0.1, 1.9]
    transforms.RandomApply([transforms.ColorJitter(saturation=0.9),],p=0.5), # 以0.5的概率调整图像的饱和度,饱和度因子的范围为[0.1, 1.9]
    transforms.ToTensor(), # 将图像转换成张量
    transforms.Normalize(image_mean, image_std), # 对图像进行标准化处理
])
# 定义验证集的数据预处理操作
transform_valid = transforms.Compose([
    transforms.Resize((224,224)), # 将图像大小调整为224*224
    transforms.ToTensor(), # 将图像转换成张量
    transforms.Normalize(image_mean, image_std), # 对图像进行标准化处理
])

然后,我们可以使用自定义数据集类来加载数据。

train_data = AppleDataset(train_path,transform_train)
valid_data = AppleDataset(val_path,transform_valid)
print("类别: {}".format(classes_name))
print("训练集: {}".format(len(train_data)))
print("验证集: {}".format(len(valid_data)))


从输出结果可以看到,数据集大约有1万张图片,我将其以8:2的比例分为训练集和测试集,这样可以防止过拟合

类别: ['d1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9']
训练集: 8165
验证集: 2046


接下来,使用PyTorch的DataLoader加载训练集和验证集的数据。你可以根据需要调整batch_sizenum_workers参数。(win的num_workers可能只能设为0,如果出现显存问题可以调小batch_size

# 使用PyTorch的DataLoader加载训练集数据
train_loader = torch.utils.data.DataLoader(
        train_data, batch_size=64, shuffle=True, num_workers= 8, pin_memory = True)
val_loader = torch.utils.data.DataLoader(
        valid_data, batch_size=64, shuffle=False, num_workers= 8, pin_memory = True)

自定义CNN模型

在这个部分,我们将使用PyTorch Image Models (timm)库来快速构建CNN模型。timm是由Ross Wightman创建的深度学习库,集成了最先进的计算机视觉模型、图层、实用程序、优化器、调度程序、数据加载器、增强和训练/验证脚本。该库可以复现ImageNet训练结果,并且方便地用于构建模型并修改类别和通道等参数。

下面是一个使用timm构建resnet34模型的示例:

import timm 
# 使用timm库中的resnet34模型
model = timm.create_model('resnet34')
# 生成一张224x224的RGB图像
x     = torch.randn(1, 3, 224, 224)
# 进行推理并打印输出的结果的形状
print(model(x).shape)

输出结果应为:

torch.Size([1, 1000])

输出的形状是正确的,是一个大小为[1, 1000]的张量,表示模型有1000个输出类别。然而,我们的数据集只有9个类别,并不是ImageNet的1000个类别。timm提供了一个参数来快速修改最后一个全连接层的类别数量。此外,我们还可以加载预训练的权重,以进行迁移学习。在这个例子中,我们使用resnet18作为示例,先确保整个流程正常运行:

model = timm.create_model('resnet18', num_classes=9, pretrained=True)
x     = torch.randn(1, 3, 224, 224)
model(x).shape

输出结果应为:

Downloading model.safetensors: 100% 46.8M/46.8M [00:03<00:00, 17.2MB/s]
torch.Size([1, 9])

如此一来,我们成功构建了一个resnet18模型,并将类别数设置为9。同时,我们还加载了预训练的权重,这有助于进行迁移学习。

如果需要了解更多关于timm的使用方法,请参考timm的文档和仓库:

timm文档:https://timm.fast.ai

timm仓库:https://github.com/rwightman/pytorch-image-models

模型训练与验证

在进行模型训练之前,我们首先要定义损失函数和优化器。在这个示例中,我们使用交叉熵损失函数和SGD优化器。

# 定义训练的超参数
epochs = 10
optimizer = torch.optim.SGD(model.parameters(), lr=0.05, momentum=0.9)
# 初始化损失函数
if cuda:
    loss_fn = nn.CrossEntropyLoss().cuda()
else:
    loss_fn = nn.CrossEntropyLoss()

接下来为了训练方便,定义了一个函数,用于计算模型的准确率:

def get_acc(outputs, label):
    """
    计算模型的准确率
    Args:
    - outputs: Tensor类型,模型的输出
    - label: Tensor类型,标签
    Returns:
    - acc: float类型,模型的准确率
    """
    total = outputs.shape[0]
    probs, pred_y = outputs.data.max(dim=1) # 得到概率
    correct = (pred_y == label).sum().data
    acc = correct / total
    return acc

然后,我们开始进行模型的训练,在每个epoch结束后,我们可以根据需要保存模型的状态,以便稍后进行推理或继续训练。

在训练过程中,我们还可以添加验证步骤来评估模型在验证集上的性能。

epoch_step = len(train_loader)
if epoch_step == 0:
    raise ValueError("训练集过小,无法进行训练,请扩充数据集,或者减小batchsize")
epoch_step_val = len(val_loader)
if cuda:
    model.cuda()
os.makedirs('checkpoint', exist_ok=True)
best_acc = 0
for epoch in range(epochs):
    model.train()
    train_loss = 0
    train_acc = 0
    print('Start Train')
    with tqdm(total=epoch_step,desc=f'Epoch {epoch + 1}/{epochs}',postfix=dict,mininterval=0.3) as pbar:
        for step,(im,label) in enumerate(train_loader,start=0):
            with torch.no_grad():
                if cuda:
                    im = im.cuda()
                    label = label.cuda()
            #----------------------#
            #   清零梯度
            #----------------------#
            optimizer.zero_grad()
            #----------------------#
            #   前向传播forward
            #----------------------#
            outputs = model(im)
            #----------------------#
            #   计算损失
            #----------------------#
            loss = loss_fn(outputs,label)
            #----------------------#
            #   反向传播
            #----------------------#
            # backward
            loss.backward()
            # 更新参数
            optimizer.step()
            train_loss += loss.data
            train_acc += get_acc(outputs,label)
            lr = optimizer.param_groups[0]['lr']
            # 在合适的位置添加以下代码
            pbar.set_postfix(**{
                'Train Loss': "{:.6f}".format(train_loss.item() / (step + 1)),
                'Train Acc': "{:.6f}".format(train_acc.item() / (step + 1)),
                'Lr': "{:.6f}".format(lr),
                'Memory': "{:.3g}G".format(torch.cuda.memory_reserved() / 1E9)
            })
            pbar.update(1)
    train_loss = train_loss.item() / len(train_loader)
    train_acc = train_acc.item() * 100 / len(train_loader)
    acc = train_acc
    print("Epoch: {}, Train Loss: {:.6f}, Train Acc: {:.6f}".format(epoch+1, train_loss, train_acc))
    state = {
        'net': model.state_dict(),
        'acc': acc,
        'epoch': epoch+1,
        'optimizer': optimizer.state_dict(),
    }
    torch.save(state, './checkpoint/last_resnet34_ckpt.pth')  # 模型保存路径
    if epoch_step_val != 0:
        model.eval()
        val_loss = 0
        val_acc = 0
        print('Start Val')
        #--------------------------------
        #   相同方法,同train
        #--------------------------------
        with tqdm(total=epoch_step_val,desc=f'Epoch {epoch + 1}/{epochs}',postfix=dict,mininterval=0.3) as pbar2:
            for step,(im,label) in enumerate(val_loader,start=0):
                with torch.no_grad():
                    if cuda:
                        im = im.cuda()
                        label = label.cuda()
                    #----------------------#
                    #   前向传播
                    #----------------------#
                    outputs = model(im)
                    loss = loss_fn(outputs,label)
                    val_loss += loss.data
                    val_acc += get_acc(outputs,label)
                    pbar2.set_postfix(**{'Val Loss': "{:.6f}".format(val_loss.item() / (step + 1)),
                                        'Val Acc': "{:.6f}".format(val_acc.item()/(step+1)),
                                        'Memory': "{:.3g}G".format(torch.cuda.memory_reserved() / 1E9)
                                        })
                    pbar2.update(1)
        lr = optimizer.param_groups[0]['lr']
        val_acc = val_acc.item() * 100 / len(val_loader)
        val_loss = val_loss.item() / len(val_loader)
        acc = val_acc
        print("Epoch: {}, Val Loss: {:.6f}, Val Acc: {:.6f}".format(epoch+1, val_loss, val_acc))
    # Save checkpoint.
    if acc > best_acc:
        print('Saving Best Model...')
        state = {
            'net': model.state_dict(),
            'acc': acc,
            'epoch': epoch+1,
            'optimizer': optimizer.state_dict(),
        }
        torch.save(state, './checkpoint/best_resnet34_ckpt.pth')
        best_acc = acc
Start Train
Epoch 1/10: 100%|██████████| 128/128 [00:18<00:00,  7.06it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.549712, Train Loss=1.246984]
Epoch: 1, Train Loss: 1.246984, Train Acc: 54.971230
Start Val
Epoch 1/10: 100%|██████████| 32/32 [00:05<00:00,  5.92it/s, Memory=1.93G, Val Acc=0.765074, Val Loss=0.703293]
Epoch: 1, Val Loss: 0.703293, Val Acc: 76.507372
Saving Best Model...
Start Train
Epoch 2/10: 100%|██████████| 128/128 [00:17<00:00,  7.37it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.745348, Train Loss=0.759731]
Epoch: 2, Train Loss: 0.759731, Train Acc: 74.534816
Start Val
Epoch 2/10: 100%|██████████| 32/32 [00:04<00:00,  7.71it/s, Memory=1.93G, Val Acc=0.839828, Val Loss=0.468506]
Epoch: 2, Val Loss: 0.468506, Val Acc: 83.982801
Saving Best Model...
Start Train
Epoch 3/10: 100%|██████████| 128/128 [00:17<00:00,  7.48it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.809956, Train Loss=0.566048]
Epoch: 3, Train Loss: 0.566048, Train Acc: 80.995631
Start Val
Epoch 3/10: 100%|██████████| 32/32 [00:04<00:00,  6.73it/s, Memory=1.93G, Val Acc=0.873488, Val Loss=0.382040]
Epoch: 3, Val Loss: 0.382040, Val Acc: 87.348789
Saving Best Model...
Start Train
Epoch 4/10: 100%|██████████| 128/128 [00:19<00:00,  6.66it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.839854, Train Loss=0.477816]
Epoch: 4, Train Loss: 0.477816, Train Acc: 83.985364
Start Val
Epoch 4/10: 100%|██████████| 32/32 [00:05<00:00,  6.09it/s, Memory=1.93G, Val Acc=0.887664, Val Loss=0.329272]
Epoch: 4, Val Loss: 0.329272, Val Acc: 88.766378
Saving Best Model...
Start Train
Epoch 5/10: 100%|██████████| 128/128 [00:17<00:00,  7.33it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.862014, Train Loss=0.408341]
Epoch: 5, Train Loss: 0.408341, Train Acc: 86.201435
Start Val
Epoch 5/10: 100%|██████████| 32/32 [00:04<00:00,  6.64it/s, Memory=1.93G, Val Acc=0.924757, Val Loss=0.251357]
Epoch: 5, Val Loss: 0.251357, Val Acc: 92.475742
Saving Best Model...
Start Train
Epoch 6/10: 100%|██████████| 128/128 [00:17<00:00,  7.43it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.876175, Train Loss=0.372916]
Epoch: 6, Train Loss: 0.372916, Train Acc: 87.617451
Start Val
Epoch 6/10: 100%|██████████| 32/32 [00:04<00:00,  6.86it/s, Memory=1.93G, Val Acc=0.908628, Val Loss=0.268046]
Epoch: 6, Val Loss: 0.268046, Val Acc: 90.862840
Start Train
Epoch 7/10: 100%|██████████| 128/128 [00:19<00:00,  6.54it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.881424, Train Loss=0.353227]
Epoch: 7, Train Loss: 0.353227, Train Acc: 88.142353
Start Val
Epoch 7/10: 100%|██████████| 32/32 [00:05<00:00,  6.40it/s, Memory=1.93G, Val Acc=0.936980, Val Loss=0.213115]
Epoch: 7, Val Loss: 0.213115, Val Acc: 93.698019
Saving Best Model...
Start Train
Epoch 8/10: 100%|██████████| 128/128 [00:18<00:00,  6.91it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.890658, Train Loss=0.313763]
Epoch: 8, Train Loss: 0.313763, Train Acc: 89.065802
Start Val
Epoch 8/10: 100%|██████████| 32/32 [00:05<00:00,  5.44it/s, Memory=1.93G, Val Acc=0.924301, Val Loss=0.221276]
Epoch: 8, Val Loss: 0.221276, Val Acc: 92.430067
Start Train
Epoch 9/10: 100%|██████████| 128/128 [00:17<00:00,  7.28it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.900245, Train Loss=0.303409]
Epoch: 9, Train Loss: 0.303409, Train Acc: 90.024549
Start Val
Epoch 9/10: 100%|██████████| 32/32 [00:05<00:00,  6.01it/s, Memory=1.93G, Val Acc=0.926207, Val Loss=0.237639]
Epoch: 9, Val Loss: 0.237639, Val Acc: 92.620653
Start Train
Epoch 10/10: 100%|██████████| 128/128 [00:18<00:00,  6.80it/s, Lr=0.050000, Memory=1.93G, Train Acc=0.907415, Train Loss=0.280949]
Epoch: 10, Train Loss: 0.280949, Train Acc: 90.741462
Start Val
Epoch 10/10: 100%|██████████| 32/32 [00:06<00:00,  5.20it/s, Memory=1.93G, Val Acc=0.944808, Val Loss=0.167078]
Epoch: 10, Val Loss: 0.167078, Val Acc: 94.480848
Saving Best Model...

可以看到最后验证集也可以达到94.5的准确率左右,还可以继续加油冲冲冲

对测试集进行预测

接下来,我们使用训练好的模型对测试集进行预测,并生成最终的CSV文件以进行提交。

首先,定义一个函数来进行模型的推理。

from tqdm import tqdm
def predict(test_loader, model):
    """
    进行模型的推理
    Args:
    - test_loader: DataLoader类型,测试集数据加载器
    - model: 模型
    Returns:
    - test_pred: numpy数组类型,模型的输出结果
    """
    # 将模型设为评估模式
    model.eval()
    test_pred = []
    with torch.no_grad():
        # 遍历测试集数据
        for input, _ in tqdm(test_loader):
            input = input.cuda()
            # 进行模型的推理
            output = model(input)
            test_pred.append(output.data.cpu().numpy())
    # 将模型的输出结果转换为numpy数组类型
    return np.vstack(test_pred)

接下来,加载测试集数据并进行重复10次的模型推理,并将每次推理的结果相加,这样相当于10个模型进行集成,不过是自己和自己集成。

# 获取测试集数据的路径
test_path = glob.glob(f'{root}/test/*')
# 加载测试集数据
test_data = AppleDataset(test_path, transform_valid)
# 使用PyTorch的DataLoader加载测试集数据
test_loader = torch.utils.data.DataLoader(
        test_data, batch_size=1, shuffle=False, num_workers= 0, pin_memory = True)
pred = None
print("-----------------Repeat 10 times-----------------")
# 进行10次模型推理,并将结果相加
for _ in range(10):
    if pred is None:
        pred = predict(test_loader, model)
    else:
        pred += predict(test_loader, model)


最后,将推理结果保存在一个CSV文件中,以便提交到比赛平台。

# 创建包含标签的DataFrame
submit = pd.DataFrame(
    {
        'uuid': [x.split('/')[-1] for x in test_path],
        'label': [['d1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9'][x] for x in pred.argmax(1)]
    }
)
# 生成CSV文件的时间戳
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
# 保存包含标签的DataFrame
label_csv_file = f'submit/predictions_{timestamp}.csv'
submit = submit.sort_values(by='uuid')
submit.to_csv(label_csv_file, index=None)
# 打印保存路径
print("Predictions saved to: {}".format(label_csv_file))

总结

在这一部分,我已顺利完成了苹果病害模型的训练与预测任务。使用Python和PyTorch作为编程语言,并根据需求自定义了一个名为AppleDataset的数据集类。在模型训练方面,我采用了自定义的CNN模型,并利用PyTorch的DataLoader加载了训练集和验证集的数据。此外,我还运用了PyTorch Image Models (timm)库来快速构建CNN模型,并使用了交叉熵损失函数和SGD优化器进行模型训练与验证。

完成这一任务后,我会进一步优化和改进模型。例如,可以尝试采用更复杂的模型架构、调整超参数或尝试使用不同的优化算法。此外,还可以尝试其他的数据增强方法,以提高模型的泛化能力。


相关文章
|
4月前
|
机器学习/深度学习 人工智能 算法
|
机器学习/深度学习 存储 人工智能
英伟达 H100 vs. 苹果M2,大模型训练,哪款性价比更高?
训练和微调大型语言模型对于硬件资源的要求非常高。目前,主流的大模型训练硬件通常采用英特尔的CPU和英伟达的GPU。然而,最近苹果的M2 Ultra芯片和AMD的显卡进展给我们带来了一些新的希望。
1058 0
|
1月前
|
机器学习/深度学习 人工智能 算法
AI人工智能(ArtificialIntelligence,AI)、 机器学习(MachineLearning,ML)、 深度学习(DeepLearning,DL) 学习路径及推荐书籍
AI人工智能(ArtificialIntelligence,AI)、 机器学习(MachineLearning,ML)、 深度学习(DeepLearning,DL) 学习路径及推荐书籍
73 0
|
3月前
|
机器学习/深度学习 人工智能 自然语言处理
不做数值运算、纯靠嘴炮也能机器学习?基于自然语言的全新ML范式来了
【6月更文挑战第30天】基于自然语言的VML简化了机器学习,让模型参数变为人类可读的文本,提高理解和应用性。借助大型语言模型的进展,VML能直接编码先验知识,自动选择模型类,并提供可解释的学习过程。然而,表达能力、训练优化及泛化能力的挑战仍需克服。[论文链接](https://arxiv.org/abs/2406.04344)
21 1
|
3月前
|
机器学习/深度学习 人工智能 算法
人工智能(AI)、机器学习(ML)和深度学习(DL)
人工智能(AI)、机器学习(ML)和深度学习(DL)
135 1
|
4月前
|
机器学习/深度学习 数据采集 分布式计算
【机器学习】Spark ML 对数据进行规范化预处理 StandardScaler 与向量拆分
标准化Scaler是数据预处理技术,用于将特征值映射到均值0、方差1的标准正态分布,以消除不同尺度特征的影响,提升模型稳定性和精度。Spark ML中的StandardScaler实现此功能,通过`.setInputCol`、`.setOutputCol`等方法配置并应用到DataFrame数据。示例展示了如何在Spark中使用StandardScaler进行数据规范化,包括创建SparkSession,构建DataFrame,使用VectorAssembler和StandardScaler,以及将向量拆分为列。规范化有助于降低特征重要性,提高模型训练速度和计算效率。
|
4月前
|
机器学习/深度学习 分布式计算 算法
【机器学习】Spark ML 对数据特征进行 One-Hot 编码
One-Hot 编码是机器学习中将离散特征转换为数值表示的方法,每个取值映射为一个二进制向量,常用于避免特征间大小关系影响模型。Spark ML 提供 OneHotEncoder 进行编码,输入输出列可通过 `inputCol` 和 `outputCol` 参数设置。在示例中,先用 StringIndexer 对类别特征编码,再用 OneHotEncoder 转换,最后展示编码结果。注意 One-Hot 编码可能导致高维问题,可结合实际情况选择编码方式。
|
3月前
|
机器学习/深度学习 人工智能 边缘计算
人工智能(AI)和机器学习(ML)
人工智能(AI)和机器学习(ML)
62 0
|
4月前
|
机器学习/深度学习 安全 算法
学习机器学习(ML)在网络安全中的重要性
机器学习(ML)是人工智能的一个分支,它使用算法来使计算机系统能够自动地从数据和经验中进行学习,并改进其性能,而无需进行明确的编程。机器学习涉及对大量数据的分析,通过识别数据中的模式来做出预测或决策。这些算法会不断地迭代和优化,以提高其预测的准确性。
47 0
|
11月前
|
机器学习/深度学习 算法 数据挖掘
ML |机器学习模型如何检测和预防过拟合?
ML |机器学习模型如何检测和预防过拟合?
155 0