此分类用于记录吴恩达深度学习课程的学习笔记。
课程相关信息链接如下:
- 原课程视频链接:[双语字幕]吴恩达深度学习deeplearning.ai
- github课程资料,含课件与笔记:吴恩达深度学习教学资料
- 课程配套练习(中英)与答案:吴恩达深度学习课后习题与答案
本篇为第四课第二周的课后习题和代码实践部分。
1. 理论习题
【中英】【吴恩达课后测验】Course 4 -卷积神经网络 - 第二周测验
还是先上一下链接,这周的理论习题涉及一些公式推导,但是在这篇博客里已经给出了很详细的过程。所以就不再展开这一部分了。
这周的代码演示内容较多,我们就把精力主要放在下面的实践内容中。
2. 代码实践
要提前说明的是,在吴恩达老师的课程中,本周编程作业的题目是Keras 入门与残差网络的搭建。
不过需要注意:Keras 原本是一个独立的第三方库,但在 TensorFlow 2.x 中,它已被集成成为 TensorFlow 的高级 API。
因此,在这位博主的 Keras入门与残差网络的搭建 博客中,部分导入语法在新版本中可能会报错,这只是由于版本更新导致,并不影响博客的核心内容,且残差网络的手工搭建部分仍然是正确且值得学习的。
我们在正文部分依旧还是用 PyTorch 来演示这周所学的内容。
因为内容较多,这里先简单列举一下:
- 使用 LeNet-5 进行手写数字图像识别
- 使用 AlexNet 进行手写数字图像识别,猫狗图像二分类
- 使用 VGG-16 进行猫狗图像二分类
- 使用 ResNet-18 进行猫狗图像二分类
2.1 使用 LeNet-5 进行手写数字图像识别
回看一下 LeNet-5 的网络结构:
我们在介绍它的时候就提到过,LeNet-5 的提出应用就是单通道的文档识别。
因此,我们就来看看这个二十多年前的模型在手写数字图像识别上的效果如何。
LeNet-5 的网络结构并不复杂,我们用 ReLU 替换了原始的 sigmoid/tanh,用CrossEntropyLoss 替代原始平方误差损失,这样在 PyTorch 中更适合现代训练。
最后代码如下:
/* by 01022.hk - online tools website : 01022.hk/zh/formatcss.html */ class LeNet5(nn.Module): def __init__(self, num_classes=10): super(LeNet5, self).__init__() self.conv1 = nn.Conv2d(1, 6, kernel_size=5) # 输入: 1x32x32 -> 输出: 6x28x28 self.pool = nn.AvgPool2d(kernel_size=2, stride=2) # 6x28x28 -> 6x14x14 self.conv2 = nn.Conv2d(6, 16, kernel_size=5) # 16x10x10 self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, num_classes) def forward(self, x): x = F.relu(self.conv1(x)) # 6x28x28 x = self.pool(x) # 6x14x14 x = F.relu(self.conv2(x)) # 16x10x10 x = self.pool(x) # 16x5x5 x = torch.flatten(x, 1) # 16*5*5=400 x = F.relu(self.fc1(x)) # 120 x = F.relu(self.fc2(x)) # 84 x = self.fc3(x) # 10 return x # 参数设置: criterion = nn.CrossEntropyLoss() #内置 softmax optimizer = optim.Adam(model.parameters(), lr=0.001) num_epochs = 10现在,来看看 LeNet-5 在手写数字图像识别上的效果如何:
可以看到及效果极佳,仅仅 10 轮训练,训练准确率几乎达到 100%,测试准确率也接近 100%,并且损失仍在平稳下降。
原因一方面,LeNet-5 的卷积-池化结构能够有效提取手写数字的局部到全局特征;另一方面,MNIST 数据集相对简单、规范,使得小型模型也能快速收敛并取得高精度。
2.2 AlexNet
还是先回顾一下网络结构:
实际上,AlexNet 被已经被PyTorch内置了,我们可以比较方便的调用:
/* by 01022.hk - online tools website : 01022.hk/zh/formatcss.html */ # 使用内置 AlexNetmodel = models.alexnet(pretrained=False)现在就来看看 AlexNet 在不同任务上的效果。
(1)使用 AlexNet 进行手写数字图像识别
这里先使用 AlexNet 跑 MNIST ,并不是追求更高精度,而是对比模型规模变大后,训练效率和收敛行为的变化。
因为 AlexNet 接受的输入为RGB图像,因此,在开始训练前,还需要对 MNIST 数据集进行预处理:
transform = transforms.Compose([ transforms.Resize((224, 224)), # AlexNet 需要 224x224 transforms.Grayscale(num_output_channels=3), # MNIST 是单通道,将其复制为 3 通道 transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ])运行结果是这样的:
这个结果不出意料,AlexNet 在 LeNet-5 的基本逻辑上又通过网络深度增加了非线性表达能力,但相应的,也会增加每轮的平均训练时间。
同时你会发现,在相同训练轮次下,AlexNet 的损失值相对 LeNet-5 略高,这并不意味着模型性能更差,而是由于其参数规模更大、优化难度更高,在有限轮次下尚未充分收敛。
从模型容量和表达能力上看,AlexNet 具备更高的性能上限,在更大数据规模或更充分训练条件下通常能取得更优结果。
(1)使用 AlexNet 进行猫狗图像二分类
我们先试试使用预训练的 AlexNet 来看看模型在这个任务上的上限。
AlexNetmodel = models.alexnet(pretrained=True)结果如下:
现在,我们再试试从头开始训练:
AlexNetmodel = models.alexnet(pretrained=False)结果如下:
仅仅不使用预训练参数,训练过程就明显变得困难、损失下降缓慢,前几层几乎学不到有意义的特征,表现出典型的梯度消失现象。
原因在于:AlexNet 参数规模大,但缺乏 BN 和残差等稳定训练的结构设计,在小数据集上很难把梯度有效传回前层。
因此,我们由此进行一些调试:
- 进行数据增强变相“增加数据量”,这一点在原论文也提到过。
- 缩小学习率,调试能否避免像这样一样“卡死”。
transform = transforms.Compose([ # (1)尺度裁剪 transforms.RandomResizedCrop( 224, scale=(0.8, 1.0), ratio=(0.9, 1.1) ), # (2)左右翻转 transforms.RandomHorizontalFlip(p=0.5), # (3)旋转 transforms.RandomRotation( degrees=10, interpolation=transforms.InterpolationMode.BILINEAR ), # (4)颜色抖动 transforms.ColorJitter( brightness=0.2, contrast=0.2, saturation=0.2, hue=0.05 ), transforms.ToTensor(), # (5)标准化 transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ), ]) optimizer = optim.Adam(model.parameters(), lr=0.0001)现在再来看结果:
很明显有了好转,损失稳步下降,但是又出现了过拟合倾向。
我们先不着急继续优化,来看看别的模型的表现。
2.3 VGG-16
现在,保持其他所有参数不变,只改变模型为没有预训练的 VGG-16:
model = models.vgg16(pretrained=False)再次运行,看看结果:
你会发现,在相同的参数设置下,AlexNet的验证准确率最高只能达到80%,而 VGG-16 却可以几乎稳定在80%以上。
简单总结来看:
- 在相同训练设置下,VGG-16 比 AlexNet 更容易从头训练,训练过程也更稳定。
- 原因在于 VGG-16 使用了统一的小卷积核结构,特征是逐层、渐进式学习的,梯度传播更顺畅。
因此,VGG-16 比 AlexNet 更好优化。
继续,最后登场的是 ResNet 。
2.4 ResNet-18
同样,我们先回顾一下它的网络结构:
我们对它的使用也并不陌生:
model = models.resnet18(pretrained=False) model.fc = nn.Linear(model.fc.in_features, 1)同样保持其他参数不变,来看看运行结果:
在这个实验规模下,ResNet-18 的优势还未被完全放大,但它的训练过程已经表现出明显的稳定性优势,这正是残差结构的核心价值。
2.5 对比总结
| 模型 | 结构特点 | 训练难度 | 实验表现 | 核心结论 |
|---|---|---|---|---|
| LeNet-5 | 浅层网络,卷积 + 池化 + 全连接,参数量小 | 非常容易 | 在 MNIST 上快速收敛,准确率接近 100% | 结构简单但有效,适合小图像、低复杂度任务 |
| AlexNet | 较深网络,大卷积核 + 大量全连接层,无 BN、无残差 | 困难 | 小数据集从头训练易梯度消失,需预训练或强正则 | 表达能力强,但优化不稳定,强依赖数据规模与初始化 |
| VGG-16 | 统一小卷积核(3×3)堆叠,结构规则 | 中等 | 从头训练稳定,验证准确率明显高于 AlexNet | 结构“规整”比“更浅”更重要,更易优化 |
| ResNet-18 | 残差连接(skip connection),梯度直通 | 最容易 | 训练最稳定,小数据集也能正常收敛 | 残差结构本质上解决了深层网络难训练问题 |
模型越往后发展,提升的重点不是“更强的表达能力”,而是“更容易被训练好”。
当然这并不绝对,同样有可以兼顾二者的强大技术,我们之后就会了解到。
回到现在,LeNet-5 结构简单,在低复杂度任务上非常好训;AlexNet 虽然更深,但缺乏良好的优化设计,小数据集上反而容易卡住;VGG-16 用规则的小卷积堆叠,让网络更深却更稳定;而 ResNet-18 通过残差连接给梯度开了“直通车”,彻底缓解了深层网络难训练的问题。
实际应用中,我们也能发现,在小规模数据集中,预训练权重往往比模型深度更重要,而在从头训练时,结构是否好优化比参数量大小更关键。
3.附录
3.1 训练代码 PyTorch版
import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms, models from torch.utils.data import DataLoader, random_split import matplotlib.pyplot as plt # 数据预处理 transform = transforms.Compose([ # (1)尺度裁剪 transforms.RandomResizedCrop( 224, scale=(0.8, 1.0), ratio=(0.9, 1.1) ), # (2)左右翻转 transforms.RandomHorizontalFlip(p=0.5), # (3)旋转 transforms.RandomRotation( degrees=10, interpolation=transforms.InterpolationMode.BILINEAR ), # (4)颜色抖动 transforms.ColorJitter( brightness=0.2, contrast=0.2, saturation=0.2, hue=0.05 ), # (5)转 Tensor transforms.ToTensor(), # (6)标准化 transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ), ]) dataset = datasets.ImageFolder(root='./cat_dog', transform=transform) train_size = int(0.8 * len(dataset)) val_size = int(0.1 * len(dataset)) test_size = len(dataset) - train_size - val_size train_dataset, val_dataset, test_dataset = random_split(dataset, [train_size, val_size, test_size]) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False) test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # ---------------------------------- # 重点:模型选择 # ---------------------------------- # model = models.alexnet(pretrained=False) # 使用 AlexNet # model = models.vgg16(pretrained=False) # 使用 VGG16 model = models.resnet18(pretrained=False) # 使用 ResNet18 # 替换最后一层分类器 AlexNet、VGG16 使用这行 # model.classifier[6] = nn.Linear(model.classifier[6].in_features, 1) # 替换最后一层分类器 ResNet18 使用这行 model.fc = nn.Linear(model.fc.in_features, 1) model = model.to(device) # 训练参数 criterion = nn.BCEWithLogitsLoss() optimizer = optim.Adam(model.parameters(), lr=0.0001) epochs = 80 train_losses = [] train_accs = [] val_accs = [] for epoch in range(epochs): model.train() epoch_train_loss = 0 correct_train = 0 total_train = 0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device).float().unsqueeze(1) outputs = model(images) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() epoch_train_loss += loss.item() preds = (outputs > 0.5).int() correct_train += (preds == labels.int()).sum().item() total_train += labels.size(0) avg_train_loss = epoch_train_loss / len(train_loader) train_acc = correct_train / total_train train_losses.append(avg_train_loss) train_accs.append(train_acc) # 验证集 model.eval() correct_val = 0 total_val = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device).float().unsqueeze(1) outputs = model(images) preds = (outputs > 0).int() correct_val += (preds == labels.int()).sum().item() total_val += labels.size(0) val_acc = correct_val / total_val val_accs.append(val_acc) print(f"轮次 [{epoch+1}/{epochs}] " f"训练损失: {avg_train_loss:.4f} " f"训练准确率: {train_acc:.4f} " f"验证准确率: {val_acc:.4f}") # 可视化 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.figure(figsize=(10,5)) plt.plot(train_losses, label='训练损失') plt.plot(train_accs, label='训练准确率') plt.plot(val_accs, label='验证准确率') plt.legend() plt.grid(True) plt.show() # 测试集 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device).float().unsqueeze(1) outputs = model(images) preds = (outputs > 0).int() correct += (preds == labels.int()).sum().item() total += labels.size(0) print(f"测试准确率: {correct / total:.4f}")3.2 训练代码 TF版
由于官方模型库并未提供 AlexNet 与 ResNet-18,我们分别采用手工实现(AlexNet)与结构等价模型(ResNet50)进行对比。
import tensorflow as tf from tensorflow.keras import layers, models import matplotlib.pyplot as plt data_augmentation = tf.keras.Sequential([ layers.Resizing(256, 256), layers.RandomCrop(224, 224), layers.RandomFlip("horizontal"), layers.RandomRotation(0.05), layers.RandomContrast(0.2), ]) train_ds = tf.keras.preprocessing.image_dataset_from_directory( "./cat_dog", validation_split=0.2, subset="training", seed=42, image_size=(224, 224), batch_size=32 ) val_ds = tf.keras.preprocessing.image_dataset_from_directory( "./cat_dog", validation_split=0.2, subset="validation", seed=42, image_size=(224, 224), batch_size=32 ) AUTOTUNE = tf.data.AUTOTUNE train_ds = train_ds.prefetch(AUTOTUNE) val_ds = val_ds.prefetch(AUTOTUNE) # ------------------------------- # 模型选择 # ------------------------------- # ---------- AlexNet ---------- # def build_alexnet(): # return models.Sequential([ # layers.Input(shape=(224, 224, 3)), # data_augmentation, # layers.Rescaling(1./255), # # layers.Conv2D(96, 11, strides=4, activation='relu'), # layers.MaxPooling2D(3, strides=2), # # layers.Conv2D(256, 5, padding='same', activation='relu'), # layers.MaxPooling2D(3, strides=2), # # layers.Conv2D(384, 3, padding='same', activation='relu'), # layers.Conv2D(384, 3, padding='same', activation='relu'), # layers.Conv2D(256, 3, padding='same', activation='relu'), # layers.MaxPooling2D(3, strides=2), # # layers.Flatten(), # layers.Dense(4096, activation='relu'), # layers.Dropout(0.5), # layers.Dense(4096, activation='relu'), # layers.Dropout(0.5), # layers.Dense(1) # ]) # model = build_alexnet() # ---------- VGG-16 ---------- # base_model = tf.keras.applications.VGG16( # include_top=False, # weights=None, # input_shape=(224, 224, 3) # ) # model = models.Sequential([ # layers.Input(shape=(224, 224, 3)), # data_augmentation, # layers.Rescaling(1./255), # base_model, # layers.Flatten(), # layers.Dense(4096, activation='relu'), # layers.Dropout(0.5), # layers.Dense(4096, activation='relu'), # layers.Dropout(0.5), # layers.Dense(1) # ]) # ---------- ResNet50 ---------- base_model = tf.keras.applications.ResNet50( include_top=False, weights=None, input_shape=(224, 224, 3) ) model = models.Sequential([ layers.Input(shape=(224, 224, 3)), data_augmentation, layers.Rescaling(1./255), base_model, layers.GlobalAveragePooling2D(), layers.Dense(1) ]) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4), loss=tf.keras.losses.BinaryCrossentropy(from_logits=True), metrics=["accuracy"] ) history = model.fit( train_ds, validation_data=val_ds, epochs=80 ) plt.figure(figsize=(10, 5)) plt.plot(history.history["loss"], label="训练损失") plt.plot(history.history["accuracy"], label="训练准确率") plt.plot(history.history["val_accuracy"], label="验证准确率") plt.legend() plt.grid(True) plt.show()