找回密码
 立即注册
首页 业界区 业界 吴恩达深度学习课程一:神经网络和深度学习 第二周:神 ...

吴恩达深度学习课程一:神经网络和深度学习 第二周:神经网络基础 课后习题和代码实践

葛雅隽 昨天 22:35
此分类用于记录吴恩达深度学习课程的学习笔记。
课程相关信息链接如下:

  • 原课程视频链接:[双语字幕]吴恩达深度学习deeplearning.ai
  • github课程资料,含课件与笔记:吴恩达深度学习教学资料
  • 课程配套练习(中英)与答案:吴恩达深度学习课后习题与答案
本篇为第一课第二周的课程习题部分的讲解。
1.理论习题

【中英】【吴恩达课后测验】Course 1 - 神经网络和深度学习 - 第二周测验
这是本周理论部分的习题和相应解析,博主已经做好了翻译和解析工作,因此便不再重复,但有一题涉及到代码中矩阵的两种乘法,我们把这道题单独拿出看一下:
看一下下面的代码:
  1. a = np.random.randn(3, 3)
  2. b = np.random.randn(3, 1)
  3. c = a * b
复制代码
请问c的维度会是多少?
这便是这道题的内容,按照我们的学习的线代知识,我们应该会得到结果为 \((3,1)\) ,但实际上这道题的答案是 \((3,3)\)
这便涉及到在python代码中对矩阵的两类乘法,以题里的量为例:
  1. c = a * b #Hadamard积(逐元素积)
  2. c = a @ b #内积
复制代码
这是两种不同的乘法,也会带来不同结果,我们用实际代码证明一下:
  1. import numpy as np
  2. # 定义矩阵 a 和列向量 b
  3. a = np.array([[1, 1, 1],
  4.               [1, 1, 1],
  5.               [1, 1, 1]])
  6. b = np.array([[1],
  7.               [2],
  8.               [3]])
  9. # 逐元素乘法 (Hadamard积)
  10. c_hadamard = a * b
  11. print("Hadamard积结果 (a * b):")
  12. print(c_hadamard)
  13. print("Hadamard积的维度:", c_hadamard.shape)
  14. # 矩阵乘法 (内积)
  15. c_dot = a @ b
  16. print("\n矩阵乘法结果 (a @ b):")
  17. print(c_dot)
  18. print("矩阵乘法的维度:", c_dot.shape)
复制代码
直接看结果:
1.png

现在来解释一下,我们可以通过结果得知,我们在线代中学习的内积运算在代码中被定义为 \(@\)
而直接使用  \(*\)  符号来运算,实际上是对 \(*\)  前的矩阵的每个元素分别做乘法。
结合广播机制,我们可以知道 \(c = a * b\) 的实际运算过程是这样的:

  • 经过广播机制, \(b\)  的内容以相同的形式向下复制两行,让维度和 \(a\) 一样变为 \((3,3)\)
  • \(a和b\) 的相同位置相乘得到结果放入 \(c\) 的相同位置,用公式来说就是:

\[c_{ij} = a_{ij}*b_{ij}\]
现在我们可以总结一下两种乘法的区别如下:

  • \(@\) 是线代中的内积乘法,当两个矩阵的维度为\((m,n),(n,k)\) 即前一矩阵的列数等于后一矩阵的行数时才可进行运算,内积会改变矩阵的维度,结果矩阵的维度为 \((m,k)\)
  • \(*\)  是逐个乘法,实际上是对 \(*\)  前的矩阵的每个元素分别做乘法,当两个矩阵使用逐个乘法运算时,python会自动将后一矩阵广播至前一矩阵大小,Hadamard积不会改变矩阵维度大小,结果矩阵和前一矩阵维度相同。
2.编程题:实现具有神经网络思维的Logistic回归

【中文】【吴恩达课后编程作业】Course 1 - 神经网络和深度学习 - 第二周作业
同样,这是整理了课程习题的博主的编码答案,其思路逻辑和可视化部分都非常完美,在不借助很多现在流行框架的情况下手动构建线性组合,激活函数等实现逻辑回归。
因此我便不再重复,这里给出我的另一个版本以供参考,会更偏向于使用目前普遍使用的框架和内置函数来构建模型,偏向展示一个较为通用的模型训练流程
本次构建我们使用比较普遍的pytorch框架来实现这个模型,而配置pytorch环境在不熟悉的情况下可能会比较困难且繁琐,如果希望动手实操但还没有配置相关环境,这里我推荐按下面这位up主的视频过程配置pytorch环境来进行练习:
【2025年最新版】手把手教你安装PyTorch,用最简单的方式教你安装PyTorch_哔哩哔哩_bilibili
后面的课程笔记部分还会再介绍另一个主流框架:tensorflow
2.1 数据准备

使用猫狗二分类数据集,其下载地址为:猫狗图像分类数据集
数据集共2400幅图像,其优点为猫狗各1200幅,做到了样本均衡。
但缺点也存在,数据规模不大,且每幅图像大小不一,需要我们进行一定的预处理。
我们便以此数据集训练一个可以分类猫狗的Logistic回归模型。
2.2 代码逻辑

完整的代码会附在文末
2.2.1 导入所需库

我们来一个个看一看所需的库,并介绍它们所起的作用。
(1)torch
  1. import torch  # PyTorch 主库,提供张量计算和自动求导的核心功能
  2. import torch.nn as nn  # 神经网络模块,包含各种层和激活函数
  3. import torch.optim as optim   # 优化器模块,用于参数更新
  4. from torch.utils.data import DataLoader, random_split #数据加载与划分工具
复制代码
当我们看到这个导入格式时,可能会产生这样一个疑问:
既然已经在第一行已经导入了torch库,为什么我们还要再显式地导入torch的子模块?
对于这个问题,我们先简单解释一下python的导库语法。

  • 直接导入库
  1. import torch  #import 库名:直接导入所需库
  2. #这样导入后,我们就可以直接使用 torch.方法名(方法参数)来直接调用torch的方法。
  3. x = torch.tensor([1, 2, 3]) #创建一个张量,张量同样是库定义的一种容纳矩阵等内容的数据结构,类似之前的numpy库。
  4. #同时,如果torch存在子模块,我们也可以用 torch.子模块名.子模块方法名(方法参数)的形式调用。
  5. y = torch.nn.Sigmoid() #创建一个 Sigmoid 激活函数层,并把它赋值给变量 `y`。
复制代码

  • 逐层导入库的子模块
  1. # 这种形式实际上是为了简化代码,先继续看上部分:
  2. import torch
  3. y = torch.nn.Sigmoid() #这是直接导入的调用方式
  4. import torch.nn as nn # import 库.模块名 as 别名
  5. y = nn.Sigmoid() #这是导入子模块后的调用方式
  6. #我们可以直接用 as 后我们为这个模块起的别名直接调用其方法。
  7. #!注意,如果不使用 as 即:
  8. import torch.nn #这种方法和 import torch 的调用方式上没任何区别,相当于直接导入子模块。
  9. y = torch.nn.Sigmoid()
复制代码

  • 直接导入库的子模块或方法
  1. #这样其实也是进一步简化代码,也涉及打包时的优化
  2. #按照之前的格式,我们先进行下面的导入
  3. import torch.utils.data as data
  4. loader = data.DataLoader(dataset, batch_size=32)
  5. # DataLoader是 PyTorch 中用来加载数据的一个类,它能够高效地处理数据集的批次(batch)加载,并且支持多种数据加载策略
  6. # DataLoader(数据集,批次大小):会根据数据集 `dataset` 和指定的 `batch_size` 来加载数据,并返回一个可迭代的对象
  7. #现在我们想进一步简化,就是这种形式:
  8. from torch.utils.data import DataLoader
  9. # from 上层模块 import 类或方法名
  10. # 现在我们使用DataLoader的格式就是:
  11. loader = DataLoader(dataset, batch_size=32)
复制代码
以这样的几种导入格式,可以帮助我们实现较为精准的导入使用的模块或方法,实际上,这也是目前普遍的使用方法,甚至官方文档中使用的也是这种格式而非直接导入整个库。
(2)torchvision
  1. from torchvision import datasets, transforms
  2. # torchvision是一个与PyTorch配合使用的开源计算机视觉工具库,常用于计算机视觉领域的任务,如图像分类、目标检测、图像分割等
  3. # datasets模块,包含了多种常用的计算机视觉数据集,自动下载并加载常见的数据集,并将其转换为torch.utils.data.Dataset格式
  4. # transforms用于对图像进行预处理和数据增强。它提供了很多常见的图像转换操作,如缩放、裁剪、归一化、旋转等
复制代码
(3)matplotlib
  1. import matplotlib.pyplot as plt
  2. # matplotlib是一个强大的绘图库,用于创建静态、动态和交互式的可视化图表。
复制代码
(4)  sklearn
  1. from sklearn.metrics import accuracy_score
  2. # sklearn 是进行机器学习任务时非常基础且实用的库。它支持从数据预处理到模型评估的全流程,涵盖了各种机器学习任务。
  3. # metrics这个模块提供了许多用于评估机器学习模型性能的函数,特别是在分类任务中。
  4. # accuracy_score用于计算分类模型的准确率(Accuracy),即正确预测的样本占所有样本的比例。
复制代码
2.2.2 数据集预处理和划分

如果把训练完整的模型比作做菜,在导入所有所需库后,我们已经具备了构建模型的所有“厨具”。
而数据集,就相当于我们的“原料”。
这里也补充一下划分数据集的种类,一般,我们会把数据集划分为以下三部分:

  • 训练集:用来训练模型的数据,帮助模型学习和调整参数。
  • 验证集:用来调整模型超参数和优化模型的性能。
  • 测试集:用来最终评估模型表现的数据,检查模型的泛化能力和真实世界的适应性。
注意,这里出现了一个之前没有提到过的概念:超参数
超参数是指在模型训练开始之前由人工设定的、用于控制模型结构或学习过程的参数。与通过反向传播算法自动学习得到的模型参数(如权重、偏置)不同,超参数不会在训练过程中被更新,而是由研究者或工程师在实验中通过经验、搜索或验证集性能调优得到。
常见的超参数包括学习率、批大小、训练轮数、网络层数、正则化系数、激活函数类型等,这些我们定义的量是通过在验证集上的结果来不断调优的。
继续用做菜的例子来说的话,训练集让模型“学会做菜”,  验证集让模型“做得更好吃”,  测试集让我们知道“这道菜到底好不好吃”。
补充了一些基础知识后,我们现在继续实操部分,先看一下数据集:
2.png

3.png

可以看到,我只是用两个文件夹分别存放两类的图像,并没有在这里就划分训练集,验证集和测试集。
而且还有一个问题,那就是图片的大小不一,深度学习模型要求每个输入样本的尺寸、通道数完全一致,如果输入图片大小不一致,很多操作无法统一处理,导致特征提取混乱。
网上对构建模型有这样一个戏称:赛博接水管,不无道理,我们在输入前,一定要对样本进行预处理,处理完的样本要和“输入水管”的管口严丝合缝,不能大也不能小,而具体训练过程中也有其他体现。
Pytorch自然提供了相应方法:
  1. # transforms.Compose的作用是将多个图像变换操作串联起来,形成一个“流水线”,这样每张图片在被加载时会依次执行这些操作。
  2. transform = transforms.Compose([  
  3.     transforms.Resize((128, 128)), # 统一图片尺寸为(128,128)  
  4.     transforms.ToTensor() # 转为Tensor张量 [通道数,高,宽]
  5.     transforms.Normalize((0.5,), (0.5,))  # 标准化,把像素映射到(-1,1)防止梯度消失   
  6. ])
  7. #我们可以把transforms.Compose的返回值transform看作一个函数,输入图片,输出图片经过这些处理后的结果。
  8. # ToTensor()这一步不能省略,因为pytorch框架只接受它定义的张量结构作为输入,我们要初步使用框架,就要遵守它定义的规则。
  9. #没有 Normalize,输入在 [0,1],非常大维度的累加后,Linear 输出可能很大导致输出接近于1,从而让梯度消失。
复制代码
这样,我们就完成了预处理部分。要说明的是,这里我们仅做了一个大小统一,转化为张量的操作,在实际训练中,可能还有更多预处理的内容,我们遇到再说。
一般来说,预处理代码就是跟在导库后的第一部分内容。
在其之后的下一步,就是载入和划分数据集,来继续看:
  1. # 加载整个数据集
  2. dataset = datasets.ImageFolder(root='./cat_dog', transform=transform)
  3. # ImageFolder是一个图像数据集加载器,能够自动读取一个按照文件夹结构分类的图像数据集。
  4. # 两个参数 root即为数据集存放文件夹路径,transform即为刚刚的预处理函数,在这里作为参数自动应用于加载的每个样本。
  5. # ImageFolder会自动读取文件夹,并以文件夹名作为分类标签标注文件夹里的内容,这里就会自动将图片分为猫狗两类。
  6. # 最后我们得到的返回值dataset是一个数据集对象,每一项都是(图片的张量表示, 标签)的形式
  7. # 先设置各部分大小
  8. total_size = len(dataset)
  9. train_size = int(0.8 * total_size)   # 80% 训练集
  10. val_size = int(0.1 * total_size)     # 10% 验证集
  11. test_size = total_size - train_size - val_size  # 10% 测试集
  12. # 按设置好的比例随机划分
  13. # 这里的随机是指对某张图分入哪部分的随机,并非比例随机
  14. train_dataset, val_dataset, test_dataset = random_split(
  15.     dataset=dataset,
  16.     lengths=[train_size, val_size, test_size]
  17. )
  18. # random_split可以把一个数据集对象随机拆分成若干个子数据集。
  19. # 两个参数 dataset即为待拆分数据集,lengths即为每个子数据集的大小
  20. # 最后的返回train_dataset, val_dataset, test_dataset就是划分好的训练集,验证集和测试集。
  21. # 定义批量数据迭代器
  22. # 这里干的实际上是我们之前说了很久的向量化。
  23. train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
  24. val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)
  25. test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
  26. # DataLoader 三个参数,第一个即为要处理的数据集,batch_size是批次大小,就是我们向量化里的m
  27. # shuffle=True 表示每个 epoch 开始前,会随机打乱数据集的顺序,防止模型按固定顺序学习
  28. # 最后我们得到三个迭代器,即可用于各部分的输入,DataLoader 加载数据时,会把多个样本堆叠成 batch,即[批次,通道数,高,宽]
复制代码
要说明的一点是,我们之前在理论部分的讲解中的一次处理所有样本,就是把这里的batch_size设置为训练集大小,而我们处理完整个训练集一次是一个轮次(epoch)。
这样,一个epoch就会进行一次传播
而实际上,batch_size通常小于训练集大小,而一个批次(batch)便会传播一次,因此,在这种情况下,一个epoch就会进行多次传播
2.2.3 模型构建

这便是最核心的部分了,我们用一个类来实现模型的架构,如果类的规模较大,我们会单独创建文件存放模型类。类定义一般会包含两类方法:

  • 初始化方法:这个模型里有什么(层级,激活函数等)
  • 向前传播方法:输入进入模型后怎么走
    来看代码:
  1. class LogisticRegressionModel(nn.Module):
  2. # 类继承自nn.Module,是 PyTorch 所有模型的基类
  3.     #初始化方法
  4.     def __init__(self):  
  5.         super().__init__() #父类初始化,用于注册子模块等,涉及源码,这里当成固定即可。  
  6.         self.flatten = nn.Flatten() #把张量后三维展平为一维(通道C*高H*宽W)
  7.         self.linear = nn.Linear(128 * 128 * 3, 1) # 输入是128x128x3,输出1个加权和
  8.         # nn.Linear接受的是二维输入[batch_size, features],这里是[32,128 * 128 * 3]
  9.         # 但Linear层不需要在参数里写 batch 维度,它内部会自动处理批量输入,只关心每个样本的特征数和每个样本输出的维度,这也是广播机制的应用。
  10.         self.sigmoid = nn.Sigmoid() #激活函数
  11.    
  12.     #向前传播方法
  13.     def forward(self, x):  
  14.         # 现在,x的维度是[32,3,128,128]
  15.         x = self.flatten(x)  #1.展平
  16.         # 现在,x的维度是[32,128 * 128 * 3]
  17.         x = self.linear(x)   #2.过线性组合得到加权和
  18.         # 现在,x的维度是[32,1]
  19.         x = self.sigmoid(x)  #3.过激活函数得到输出
  20.         # 现在,x的维度是[32,1]
  21.         return x
复制代码
定义完成模型类后,我们便可以将其实例化并加以使用。
2.2.4 设备选择
  1. model = LogisticRegressionModel()  #实例化模型
  2. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  #自动选择CPU或者GPU
  3. model.to(device) #模型的所有参数(权重、偏置)都会存放在对应设备里。
复制代码
我们都知道CPU就像计算机的“大脑”,但在深度学习的模型训练领域中,反而GPU更常用,尤其是在较大的模型训练中,GPU 是深度学习的“加速引擎”,它用大量并行核心,把神经网络训练和推理中重复、耗时的矩阵运算做得又快又高效,所谓的“租用服务器让模型跑快点”,实际上就是利用服务器的较先进的GPU。
2.2.5 损失函数和优化器

我们同样使用内置函数来定义这两部分:
  1. criterion = nn.BCELoss()  # 二分类的交叉熵损失。  
  2. optimizer = optim.SGD(model.parameters(), lr=0.01) #梯度下降法,每个批次传播一次。
  3. # SGD 两个参数 model.parameters()即为模型的所有参数:权重,偏置等。
  4. # lr即为学习率。
复制代码
2.2.6 训练和验证

这部分的逻辑较为复杂,主要思路就是遍历轮次和批次来进行传播,并记录相应量用于后续画图。
  1. # 定义训练的总轮数
  2. epochs = 10  # 表示训练整个数据集的次数
  3. train_losses = []  # 用于记录每个epoch的训练损失,便于可视化
  4. val_accuracies = []  # 用于记录每个epoch验证集准确率,便于可视化
  5. # 开始训练循环,每个epoch表示遍历完整个训练集一次
  6. for epoch in range(epochs):  
  7.     model.train()  # 设置模型为训练模式
  8.     epoch_train_loss = 0  # 用于累计该epoch的总训练损失
  9.     # 遍历训练集DataLoader,每次获取一个batch
  10.     for images, labels in train_loader:  
  11.         # images: Tensor, 形状 [32, 3, 128, 128]
  12.         # labels: Tensor, 形状 [32]
  13.         images, labels = images.to(device), labels.to(device).float().unsqueeze(1)
  14.         # .to(device): 将张量移动到GPU或CPU
  15.         # .float(): 将标签转为float类型,因为BCELoss要求输入为浮点数
  16.         # .unsqueeze(1): 在第1维增加一维,使labels形状变为 [32, 1],与输出匹配
  17.         # 前向传播:输入图片,计算模型预测输出
  18.         outputs = model(images)  # 调用模型的forward方法,返回 [32, 1]
  19.         #官方推荐这样的形式,实际上相当于model.forward(images)
  20.         loss = criterion(outputs, labels)  # 计算二分类交叉熵损失,输出标量Tensor
  21.         # 反向传播与参数更新
  22.         optimizer.zero_grad()  # 清空上一次梯度,避免梯度累加
  23.         loss.backward()        # 自动求梯度,计算每个参数的梯度
  24.         optimizer.step()       # 根据梯度更新参数,完成一次优化步骤
  25.         # 累计损失,用于计算平均训练损失
  26.         epoch_train_loss += loss.item()  
  27.         # .item():将单元素Tensor转为Python浮点数,便于记录
  28.     # 计算该epoch的平均训练损失
  29.     avg_train_loss = epoch_train_loss / len(train_loader)
  30.     train_losses.append(avg_train_loss)  # 保存到列表,用于后续绘图
  31.     # 验证集评估准确率
  32.     model.eval()  # 设置模型为评估模式
  33.     val_true, val_pred = [], []  # 用于记录验证集真实标签和预测标签
  34.     with torch.no_grad():  # 禁用梯度计算,节省显存和计算量
  35.         for images, labels in val_loader:  # 遍历验证集
  36.             images = images.to(device)  # 移动到GPU/CPU
  37.             outputs = model(images)      # 前向传播得到预测概率
  38.             preds = (outputs.cpu().numpy() > 0.5).astype(int).flatten()
  39.             # outputs.cpu().numpy(): 移动到CPU并转为numpy数组
  40.             # numpy不用导入,这是PyTorch 内部实现的桥接接口
  41.             # 要用到scikit-learn、matplotlib 等库计算或可视化,这些库只接受 CPU 数据
  42.             # > 0.5: 将概率转换为0/1预测
  43.             # .astype(int): 把布尔值 True/False 转为整数 1/0
  44.             # .flatten(): 将二维数组展平成一维
  45.             val_pred.extend(preds)       # 将预测结果加入列表
  46.             val_true.extend(labels.numpy())  # 将真实标签加入列表
  47.     # 使用sklearn计算验证集准确率
  48.     val_acc = accuracy_score(val_true, val_pred)
  49.     val_accuracies.append(val_acc)  # 保存准确率,用于绘图
  50.     # 打印该epoch的训练损失和验证集准确率
  51.     print(f"轮次: [{epoch+1}/{epochs}], 训练损失: {avg_train_loss:.4f}, 验证准确率: {val_acc:.4f}")
  52.     # {变量:.4f}表示保留4位小数
复制代码
2.2.7 可视化
  1. plt.rcParams['font.sans-serif'] = ['SimHei']  
  2. # 设置全局字体为黑体(SimHei),支持中文显示
  3. # plt.rcParams 是 Matplotlib 的全局参数字典,可修改默认样式
  4. plt.rcParams['axes.unicode_minus'] = False
  5. # 设置 False 表示允许正常显示负号
  6. # 绘制曲线
  7. plt.plot(train_losses, label='训练损失')  
  8. # 绘制训练集损失曲线
  9. # plt.plot(y, label=...) 用于绘制折线图,label 用于图例说明
  10. plt.plot(val_accuracies, label='验证准确率')
  11. # 作绘制验证集准确率曲线
  12. # 设置标题与坐标轴
  13. plt.title("训练损失与验证准确率随轮次变化图") # 设置图表标题
  14. plt.xlabel("训练轮次(Epoch)") # 设置横轴标题
  15. plt.ylabel("数值") # 设置纵轴标题
  16. plt.legend() # 显示图例,用于区分不同折线
  17. # plt.legend() 会显示各 plt.plot() 的 label 内容
  18. plt.grid(True) # 开启网格显示
  19. plt.show() # 显示绘制的图形窗口
复制代码
2.2.8 最终测试

在最后用测试集进行评估之前,其实应该有根据训练集对超参数进行调优的过程,但由于目前的篇幅已经较长了,我们先看完流程,我会在最后再附上一个使用方格调优版本的代码
  1. # 模型评估(测试集)
  2. model.eval()
  3. y_true, y_pred = [], []
  4. # 定义两个空列表,用于存储测试集的真实标签与预测标签
  5. with torch.no_grad():
  6.     for images, labels in test_loader:
  7.         images = images.to(device)
  8.         outputs = model(images)
  9.         preds = (outputs.cpu().numpy() > 0.5).astype(int).flatten()
  10.         y_pred.extend(preds)
  11.         y_true.extend(labels.numpy())
  12. acc = accuracy_score(y_true, y_pred)
  13. print(f"测试准确率: {acc:.4f}")
复制代码
可以发现,验证集和测试集的代码部分几乎没有差别,二者的主要差别在于它们起到的作用上。
2.3 结果分析

4.png

这是这个模型的结果,可以发现准确率不高,只有一半左右。
而造成这个结果的因素是多样的:

  • 数据集规模不大,图片大小不一。
  • 预处理简单,可能造成失真。
  • 模型结构简单,拟合能力不强
  • 没有对超参数进行较详细地调优
    此外,还有其他影响因素。
    而下面便是可视化部分的代码结果:
    5.png

我们可能会发现这样一个问题:按照梯度下降法的思路,损失值应该一直下降才对,为什么反而会有升高的反复现象?
其实,这是一个非常常见的现象。
虽然梯度下降的理论目标是不断让损失函数下降,但在实际训练中,损失值并不会严格单调递减,原因主要有以下几点:

  • 每次迭代并不是用全部数据计算梯度,而是用一个小批量,不同批次的数据分布略有差异,会导致梯度方向有波动,因此损失可能短暂上升。
  • 如果学习率偏大,每次更新的步长过长,可能会“越过”最优点,使损失出现震荡。
  • 即使是简单模型,在高维空间中损失函数也可能存在多个局部极小值和鞍点,训练过程可能会在这些区域间来回波动。
    换句话说,总体趋势下降才是关键,出现轻微的上升是正常现象,不代表模型没有学习。
我们本篇的主要目的还是展示模型构建的过程,在之后的课程学习里,会涉及更多更复杂的算法,函数与优化等,我们到时使用其再来试试在猫狗二分类数据集上的分类效果。
最后,完整代码如下:

  • 示例版本
  1. import torch  
  2. import torch.nn as nn  
  3. import torch.optim as optim  
  4. from torchvision import datasets, transforms  
  5. from torch.utils.data import DataLoader, random_split  
  6. import matplotlib.pyplot as plt  
  7. from sklearn.metrics import accuracy_score  
  8.   
  9. transform = transforms.Compose([  
  10.     transforms.Resize((128, 128)),        
  11.     transforms.ToTensor(),                 
  12.     transforms.Normalize((0.5,), (0.5,))   
  13. ])  
  14. dataset = datasets.ImageFolder(root='./cat_dog', transform=transform)  
  15.   
  16. train_size = int(0.8 * len(dataset))  
  17. val_size = int(0.1 * len(dataset))  
  18. test_size = len(dataset) - train_size - val_size  
  19. train_dataset, val_dataset, test_dataset = random_split(dataset, [train_size, val_size, test_size])  
  20. train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)  
  21. val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)  
  22. test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)  
  23.   
  24. class LogisticRegressionModel(nn.Module):  
  25.     def __init__(self):  
  26.         super().__init__()  
  27.         self.flatten = nn.Flatten()  
  28.         self.linear = nn.Linear(128 * 128 * 3, 1)  #
  29.         self.sigmoid = nn.Sigmoid()  
  30.   
  31.     def forward(self, x):  
  32.         x = self.flatten(x)  
  33.         x = self.linear(x)  
  34.         x = self.sigmoid(x)  
  35.         return x  
  36.   
  37. model = LogisticRegressionModel()  
  38. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  
  39. model.to(device)  
  40. criterion = nn.BCELoss()   
  41. optimizer = optim.SGD(model.parameters(), lr=0.01)  
  42.   
  43. epochs = 10  
  44. train_losses = []  
  45. val_accuracies = []  
  46.   
  47. for epoch in range(epochs):  
  48.     model.train()  
  49.     epoch_train_loss = 0  
  50.     for images, labels in train_loader:  
  51.         images, labels = images.to(device), labels.to(device).float().unsqueeze(1)  
  52.   
  53.         outputs = model(images)  
  54.         loss = criterion(outputs, labels)  
  55.       
  56.         optimizer.zero_grad()  
  57.         loss.backward()  
  58.         optimizer.step()  
  59.   
  60.         epoch_train_loss += loss.item()  
  61.     avg_train_loss = epoch_train_loss / len(train_loader)  
  62.     train_losses.append(avg_train_loss)  
  63.       
  64.     model.eval()  
  65.     val_true, val_pred = [], []  
  66.     with torch.no_grad():  
  67.         for images, labels in val_loader:  
  68.             images = images.to(device)  
  69.             outputs = model(images)  
  70.             preds = (outputs.cpu().numpy() > 0.5).astype(int).flatten()  
  71.             val_pred.extend(preds)  
  72.             val_true.extend(labels.numpy())  
  73.   
  74.     val_acc = accuracy_score(val_true, val_pred)  
  75.     val_accuracies.append(val_acc)  
  76.     print(f"轮次: [{epoch+1}/{epochs}], 训练损失: {avg_train_loss:.4f}, 验证准确率: {val_acc:.4f}")  
  77.   
  78. plt.rcParams['font.sans-serif'] = ['SimHei']   
  79. plt.rcParams['axes.unicode_minus'] = False  
  80.   
  81. plt.plot(train_losses, label='训练损失')  
  82. plt.plot(val_accuracies, label='验证准确率')  
  83. plt.title("训练损失与验证准确率随轮次变化图")  
  84. plt.xlabel("训练轮次(Epoch)")  
  85. plt.ylabel("数值")  
  86. plt.legend()  
  87. plt.grid(True)  
  88. plt.show()  
  89.   
  90. model.eval()  
  91. y_true, y_pred = [], []  
  92. with torch.no_grad():  
  93.     for images, labels in test_loader:  
  94.         images = images.to(device)  
  95.         outputs = model(images)  
  96.         preds = (outputs.cpu().numpy() > 0.5).astype(int).flatten()  
  97.         y_pred.extend(preds)  
  98.         y_true.extend(labels.numpy())  
  99. acc = accuracy_score(y_true, y_pred)  
  100. print(f"测试准确率: {acc:.4f}")
复制代码

  • 加入方格搜索优化超参数的版本:
  1. import torch  
  2. import torch.nn as nn  
  3. import torch.optim as optim  
  4. from torchvision import datasets, transforms  
  5. from torch.utils.data import DataLoader, random_split  
  6. from sklearn.metrics import accuracy_score  
  7. import itertools  
  8.   
  9. transform = transforms.Compose([  
  10.     transforms.Resize((128, 128)),  
  11.     transforms.ToTensor(),  
  12.     transforms.Normalize((0.5,), (0.5,))  
  13. ])  
  14.   
  15. dataset = datasets.ImageFolder(root='./cat_dog', transform=transform)  
  16. train_size = int(0.8 * len(dataset))  
  17. val_size = int(0.1 * len(dataset))  
  18. test_size = len(dataset) - train_size - val_size  
  19.   
  20. train_dataset, val_dataset, test_dataset = random_split(dataset, [train_size, val_size, test_size])  
  21.   
  22. class LogisticRegressionModel(nn.Module):  
  23.     def __init__(self):  
  24.         super().__init__()  
  25.         self.flatten = nn.Flatten()  
  26.         self.linear = nn.Linear(128 * 128 * 3, 1)  
  27.         self.sigmoid = nn.Sigmoid()  
  28.   
  29.     def forward(self, x):  
  30.         x = self.flatten(x)  
  31.         x = self.linear(x)  
  32.         x = self.sigmoid(x)  
  33.         return x  
  34.   
  35. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  
  36.   
  37. param_grid = {  
  38.     'lr': [0.01, 0.001],  
  39.     'batch_size': [16, 32],  
  40.     'num_epochs': [5, 10]  
  41. }  
  42.   
  43. best_acc = 0  
  44. best_params = None  
  45.   
  46. for lr, batch_size, num_epochs in itertools.product(param_grid['lr'], param_grid['batch_size'], param_grid['num_epochs']):  
  47.     print(f"当前超参数: 学习率={lr}, 批次大小={batch_size}, 总轮次={num_epochs}")  
  48.   
  49.     train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)  
  50.     val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)  
  51.     test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)  
  52.   
  53.     model = LogisticRegressionModel().to(device)  
  54.     criterion = nn.BCELoss()  
  55.     optimizer = optim.SGD(model.parameters(), lr=lr)  
  56.   
  57.     for epoch in range(num_epochs):  
  58.         model.train()  
  59.         epoch_loss = 0  
  60.         for images, labels in train_loader:  
  61.             images, labels = images.to(device), labels.to(device).float().unsqueeze(1)  
  62.             outputs = model(images)  
  63.             loss = criterion(outputs, labels)  
  64.             optimizer.zero_grad()  
  65.             loss.backward()  
  66.             optimizer.step()  
  67.             epoch_loss += loss.item()  
  68.         avg_loss = epoch_loss / len(train_loader)  
  69.   
  70.         model.eval()  
  71.         y_val_true, y_val_pred = [], []  
  72.         with torch.no_grad():  
  73.             for images, labels in val_loader:  
  74.                 images = images.to(device)  
  75.                 outputs = model(images)  
  76.                 preds = (outputs.cpu().numpy() > 0.5).astype(int).flatten()  
  77.                 y_val_pred.extend(preds)  
  78.                 y_val_true.extend(labels.numpy())  
  79.         val_acc = accuracy_score(y_val_true, y_val_pred)  
  80.         print(f"轮次 {epoch+1}/{num_epochs}, 损失: {avg_loss:.4f}, 验证准确率: {val_acc:.4f}")  
  81.     if val_acc > best_acc:  
  82.         best_acc = val_acc  
  83.         best_params = {'lr': lr, 'batch_size': batch_size, 'num_epochs': num_epochs, 'model_state_dict': model.state_dict()}  
  84. print(f"\n最佳验证准确率: {best_acc:.4f} 超参数设置: {best_params}")  
  85. best_model = LogisticRegressionModel().to(device)  
  86. best_model.load_state_dict(best_params['model_state_dict'])  
  87. best_model.eval()  
  88. y_test_true, y_test_pred = [], []  
  89. with torch.no_grad():  
  90.     for images, labels in test_loader:  
  91.         images = images.to(device)  
  92.         outputs = best_model(images)  
  93.         preds = (outputs.cpu().numpy() > 0.5).astype(int).flatten()  
  94.         y_test_pred.extend(preds)  
  95.         y_test_true.extend(labels.numpy())  
  96.   
  97. test_acc = accuracy_score(y_test_true, y_test_pred)  
  98. print(f"使用最优超参数的测试准确率: {test_acc:.4f}")
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册