使用Tensorflow和CNN进行猫狗图片训练的实战总结

关于Tensorflow可参考: 认识Scikit-learn/PyTorch/TensorFlow这几个AI框架_python ai框架-CSDN博客

关于CNN可参考:

认识神经网络和深度学习-CSDN博客

CNN和图像识别有什么关系

CNN(卷积神经网络,Convolutional Neural Network)是图像识别领域中最核心、最主流的技术,二者的关系可以概括为:CNN 通过其独特的卷积机制,解决了图像识别中的关键难题,成为现代图像识别系统的 "引擎"。

一、图像识别的核心挑战

图像识别的目标是让计算机 "看懂" 图像(如判断图像中的物体类别、位置等),但原始图像存在两个核心难题:

  1. 数据维度高:一张普通的彩色图像(如 224×224 像素)包含 224×224×3=150,528 个像素值,直接用传统神经网络处理会导致参数爆炸(计算量过大)。

  2. 空间关联性强:图像的语义信息(如 "猫的耳朵""汽车轮子")依赖像素的局部空间关系(相邻像素共同构成特征),而非孤立的像素值。

二、CNN 如何适配图像识别

CNN 通过三大核心机制,完美解决了上述问题,使其成为图像识别的 "量身定制" 工具:

卷积操作:提取局部特征,保留空间关联

  • 卷积层通过卷积核滑动,对图像的局部区域(如 3×3、5×5 像素块)进行特征提取(如边缘、纹理、形状,如前文所述)。

  • 这种操作天然保留了像素的空间位置关系(相邻像素的特征会被同时处理),符合图像中 "局部像素共同构成特征" 的规律(例如 "眼睛" 由相邻的肤色、黑色像素组成)。

池化操作:降低维度,增强鲁棒性

  • 池化层(如最大池化、平均池化)通过对局部区域的特征进行聚合(如取最大值、平均值),在减少特征维度的同时,保留关键信息。

  • 例如,2×2 最大池化可将特征图尺寸缩小一半,参数和计算量大幅降低,同时让模型对微小的位置变化(如物体轻微位移)更鲁棒(不敏感)。

层级特征提取:从 "像素" 到 "语义" 的递进

  • CNN 的多层结构(浅层→深层)会自动学习层级化的特征:

    • 浅层(如前几层卷积)提取基础特征(边缘、纹理、颜色块);

    • 中层(如中间卷积层)组合基础特征,形成部件级特征(如 "猫的耳朵轮廓""汽车的车窗");

    • 深层(如全连接层前的卷积层)进一步组合,得到抽象的语义特征(如 "猫""汽车" 的整体特征)。

  • 这种层级特征与人类视觉系统的认知规律一致(人类先看到局部细节,再整合为整体)。

三、CNN 推动图像识别的突破

在 CNN 出现前,图像识别依赖人工设计特征(如 SIFT、HOG 等),识别准确率低(如 2012 年 ImageNet 竞赛的传统方法准确率仅 50% 左右)。

2012 年,以 AlexNet 为代表的 CNN 首次在 ImageNet 竞赛中超越传统方法,将错误率从 26% 降至 15%,此后 CNN 推动图像识别准确率持续突破:

  • 如今主流 CNN 模型(如 ResNet、EfficientNet)在 ImageNet 等大型数据集上的准确率已接近人类水平(97% 以上);

  • 基于 CNN 的图像识别技术已广泛应用于人脸识别、自动驾驶(识别行人 / 红绿灯)、医学影像诊断(识别肿瘤)等领域。

四、总结:CNN 是图像识别的 "核心技术支柱"

  • 依赖关系:现代图像识别系统(尤其是高精度系统)几乎都以 CNN 为基础架构,没有 CNN,就没有当前图像识别的实用化水平。

  • 本质作用:CNN 通过卷积、池化等操作,将高维度、强关联的图像数据转化为可用于分类 / 识别的抽象特征,让计算机能高效 "理解" 图像的语义信息。

简单来说:图像识别是目标,CNN 是实现这一目标的最有效工具。

Tensorflow和CNN之间的关系

TensorFlow 和 CNN(卷积神经网络)是深度学习领域中工具与技术的关系:

  • TensorFlow是一个开源的深度学习框架,提供了构建、训练和部署神经网络的工具和 API;
  • CNN是一种专门为处理具有网格结构数据(如图像)而设计的神经网络架构。

TensorFlow 可以用来实现 CNN,而 CNN 则是 TensorFlow 在计算机视觉任务中最常见的应用场景之一。下面从技术细节、应用场景和实践案例三个维度展开说明:

一、技术细节:TensorFlow 如何支持 CNN?

TensorFlow 为实现 CNN 提供了以下核心功能:

  1. 卷积层实现

TensorFlow 内置了多种卷积操作,如:

  • tf.nn.conv2d:二维卷积,用于处理图像(最常用);
  • tf.nn.conv3d:三维卷积,用于处理视频或体积数据;
  • 支持自定义卷积核大小(如 3×3、5×5)、步长(stride)、填充方式(padding)等参数。

示例代码(简化版):

python 复制代码
import tensorflow as tf

# 输入:批量大小=32,高度=224,宽度=224,通道数=3(RGB)
x = tf.random.normal([32, 224, 224, 3])
# 定义32个3×3的卷积核
filters = tf.Variable(tf.random.normal([3, 3, 3, 32]))
# 执行卷积操作
conv_output = tf.nn.conv2d(x, filters, strides=[1, 1, 1, 1], padding='SAME')
  1. 池化层实现

支持最大池化(Max Pooling)、平均池化(Average Pooling)等操作:

  • tf.nn.max_pool2d:二维最大池化;
  • tf.nn.avg_pool2d:二维平均池化。

示例代码

python 复制代码
pool_output = tf.nn.max_pool2d(conv_output, ksize=[1, 2, 2, 1], 
                              strides=[1, 2, 2, 1], padding='VALID')
  1. 高级 API 封装

TensorFlow 的 Keras 接口进一步简化了 CNN 的构建:

python 复制代码
from tensorflow.keras import layers, models

model = models.Sequential([
    layers.Conv2D(32, (3, 3), activation='relu', input_shape=(224, 224, 3)),
    layers.MaxPooling2D((2, 2)),
    layers.Conv2D(64, (3, 3), activation='relu'),
    layers.MaxPooling2D((2, 2)),
    layers.Flatten(),
    layers.Dense(64, activation='relu'),
    layers.Dense(10, activation='softmax')  # 假设10分类任务
])
  1. 预训练模型支持

TensorFlow 提供了多种预训练的 CNN 模型(如 ResNet、VGG、MobileNet),可直接用于迁移学习

python 复制代码
from tensorflow.keras.applications import ResNet50

base_model = ResNet50(weights='imagenet', include_top=False)

二、应用场景:TensorFlow+CNN 的典型案例

  1. 图像分类:如 CIFAR-10、ImageNet 等数据集上的分类任务。
  2. 目标检测:结合 Faster R-CNN、YOLO 等算法,在 TensorFlow 中实现物体定位与识别。
  3. 语义分割:使用 U-Net、DeepLab 等模型对图像进行像素级分类。
  4. 人脸识别:通过 CNN 提取人脸特征,实现身份验证。
  5. 自动驾驶:处理车载摄像头图像,识别道路、车辆和行人。

三、实践案例:用 TensorFlow 实现 CNN 图像分类

以下是一个完整的 TensorFlow+CNN 图像分类示例(基于 MNIST 手写数字数据集):

python 复制代码
import tensorflow as tf
from tensorflow.keras import layers, models

# 1. 加载数据
mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

# 2. 数据预处理
x_train, x_test = x_train / 255.0, x_test / 255.0
x_train = x_train.reshape(-1, 28, 28, 1)  # 添加通道维度
x_test = x_test.reshape(-1, 28, 28, 1)

# 3. 构建CNN模型
model = models.Sequential([
    layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
    layers.MaxPooling2D((2, 2)),
    layers.Conv2D(64, (3, 3), activation='relu'),
    layers.MaxPooling2D((2, 2)),
    layers.Flatten(),
    layers.Dense(64, activation='relu'),
    layers.Dense(10, activation='softmax')
])

# 4. 编译模型
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

# 5. 训练模型
model.fit(x_train, y_train, epochs=5, validation_data=(x_test, y_test))

# 6. 评估模型
test_loss, test_acc = model.evaluate(x_test, y_test)
print(f"Test accuracy: {test_acc * 100:.2f}%")

四、总结

TensorFlow 是实现 CNN 的强大工具,它提供了底层的张量运算和高层的 API 封装,使研究者和工程师能够高效地构建、训练和部署各种 CNN 模型。而 CNN 则是 TensorFlow 在计算机视觉领域的核心应用方向,二者结合推动了图像识别、目标检测等任务的快速发展。

安装相关python库

直接参考:认识NumPy/Scipy/Pandas/Matplotlib这几个Python库-CSDN博客

安装Tensorflow

直接参考:

Tensorflow的安装记录-CSDN博客

实战操作

下面是一个使用 TensorFlow 和 CNN 进行猫狗识别的完整示例,使用国内镜像源加载数据,并确保代码可以直接执行。这个示例包含数据加载、模型构建、训练和评估的完整流程,同时添加了详细的注释以便理解。

这个代码示例具有以下特点:

  1. 国内源加载:使用 try-except 结构尝试多种加载方式,优先使用国内镜像源。

  2. 完整流程:包含数据加载、预处理、模型构建、训练、评估和可视化的完整流程。

  3. 兼容性:移除了可能导致兼容性问题的参数,确保代码可直接执行。

  4. 可视化:包含数据样本和模型预测结果的可视化,方便理解。

  5. 模型优化:使用 dropout 层防止过拟合,并添加早停和模型检查点回调。

你可以直接运行这个脚本,它会自动尝试从国内源加载数据并开始训练。如果遇到下载问题,脚本会提供相应的错误信息和解决建议。训练完成后,模型会保存为cat_dog_model.h5,同时会显示训练过程和预测结果的可视化图表。

第一版程序如下:

python 复制代码
import tensorflow as tf
import tensorflow_datasets as tfds
from tensorflow.keras import layers, models, callbacks
import matplotlib.pyplot as plt
import os

# 设置中文显示
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]

# 定义常量
IMAGE_SIZE = (150, 150)
BATCH_SIZE = 32
EPOCHS = 15
NUM_CLASSES = 2

# 尝试从国内镜像源加载数据集
try:
    print("正在从国内镜像源加载数据集...")
    (ds_train, ds_val, ds_test), ds_info = tfds.load(
        'cats_vs_dogs',
        split=['train[:80%]', 'train[80%:90%]', 'train[90%:]'],
        shuffle_files=True,
        as_supervised=True,
        with_info=True,
        try_gcs=True  # 尝试从Google Cloud Storage下载
    )
except Exception as e:
    print(f"自动下载失败: {e}")
    print("尝试使用替代方法加载数据集...")
    
    # 替代方法:手动指定下载URL
    config = tfds.download.DownloadConfig(
        # 使用Hugging Face镜像源
        manual_dir=None,
        download_mode=tfds.GenerateMode.REUSE_DATASET_IF_EXISTS
    )
    
    try:
        (ds_train, ds_val, ds_test), ds_info = tfds.load(
            'cats_vs_dogs',
            split=['train[:80%]', 'train[80%:90%]', 'train[90%:]'],
            shuffle_files=True,
            as_supervised=True,
            with_info=True,
            download_and_prepare_kwargs={'download_config': config}
        )
    except Exception as e2:
        print(f"仍然无法加载数据集: {e2}")
        print("请确保tensorflow_datasets版本 >= 4.0,或手动下载数据集")
        print("数据集下载地址: https://www.kaggle.com/c/dogs-vs-cats/data")
        exit(1)

# 数据预处理函数
def preprocess_image(image, label):
    image = tf.cast(image, tf.float32)
    image = tf.image.resize(image, IMAGE_SIZE)
    image = image / 255.0  # 归一化到[0,1]
    return image, label

# 构建数据加载流水线
ds_train = ds_train.map(preprocess_image, num_parallel_calls=tf.data.AUTOTUNE)
ds_train = ds_train.cache()
ds_train = ds_train.shuffle(ds_info.splits['train'].num_examples * 0.8)
ds_train = ds_train.batch(BATCH_SIZE)
ds_train = ds_train.prefetch(tf.data.AUTOTUNE)

ds_val = ds_val.map(preprocess_image, num_parallel_calls=tf.data.AUTOTUNE)
ds_val = ds_val.batch(BATCH_SIZE)
ds_val = ds_val.cache()
ds_val = ds_val.prefetch(tf.data.AUTOTUNE)

ds_test = ds_test.map(preprocess_image, num_parallel_calls=tf.data.AUTOTUNE)
ds_test = ds_test.batch(BATCH_SIZE)
ds_test = ds_test.prefetch(tf.data.AUTOTUNE)

# 可视化样本
def visualize_samples(dataset):
    plt.figure(figsize=(10, 10))
    for images, labels in dataset.take(1):
        for i in range(9):
            ax = plt.subplot(3, 3, i + 1)
            plt.imshow(images[i].numpy())
            plt.title("猫" if labels[i].numpy() == 0 else "狗")
            plt.axis("off")
    plt.tight_layout()
    plt.show()

print("数据样本可视化:")
visualize_samples(ds_train)

# 构建CNN模型
def build_model():
    model = models.Sequential([
        # 卷积层和池化层
        layers.Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3)),
        layers.MaxPooling2D((2, 2)),
        layers.Conv2D(64, (3, 3), activation='relu'),
        layers.MaxPooling2D((2, 2)),
        layers.Conv2D(128, (3, 3), activation='relu'),
        layers.MaxPooling2D((2, 2)),
        
        # 全连接层
        layers.Flatten(),
        layers.Dense(128, activation='relu'),
        layers.Dropout(0.5),  # 防止过拟合
        layers.Dense(NUM_CLASSES, activation='softmax')
    ])
    
    # 编译模型
    model.compile(
        optimizer='adam',
        loss='sparse_categorical_crossentropy',
        metrics=['accuracy']
    )
    
    return model

# 创建模型
model = build_model()
print("模型结构:")
model.summary()

# 设置回调函数
callbacks_list = [
    callbacks.EarlyStopping(
        monitor='val_loss',
        patience=3,  # 如果验证集损失3个epoch不下降则停止训练
        restore_best_weights=True
    ),
    callbacks.ModelCheckpoint(
        filepath='cat_dog_model.h5',
        monitor='val_accuracy',
        save_best_only=True,
        verbose=1
    ),
    callbacks.TensorBoard(log_dir='./logs')
]

# 训练模型
print("开始训练模型...")
history = model.fit(
    ds_train,
    epochs=EPOCHS,
    validation_data=ds_val,
    callbacks=callbacks_list
)

# 评估模型
print("在测试集上评估模型...")
test_loss, test_acc = model.evaluate(ds_test)
print(f"测试集准确率: {test_acc:.4f}")

# 绘制训练历史
def plot_training_history(history):
    plt.figure(figsize=(12, 4))
    
    # 绘制准确率曲线
    plt.subplot(1, 2, 1)
    plt.plot(history.history['accuracy'], label='训练准确率')
    plt.plot(history.history['val_accuracy'], label='验证准确率')
    plt.xlabel('Epoch')
    plt.ylabel('准确率')
    plt.legend()
    plt.title('训练和验证准确率')
    
    # 绘制损失曲线
    plt.subplot(1, 2, 2)
    plt.plot(history.history['loss'], label='训练损失')
    plt.plot(history.history['val_loss'], label='验证损失')
    plt.xlabel('Epoch')
    plt.ylabel('损失')
    plt.legend()
    plt.title('训练和验证损失')
    
    plt.tight_layout()
    plt.show()

plot_training_history(history)

# 预测示例
def predict_example(model, dataset):
    plt.figure(figsize=(12, 12))
    for images, labels in dataset.take(1):
        predictions = model.predict(images)
        for i in range(9):
            ax = plt.subplot(3, 3, i + 1)
            plt.imshow(images[i].numpy())
            
            # 获取预测结果
            predicted_class = "猫" if tf.argmax(predictions[i]) == 0 else "狗"
            confidence = tf.reduce_max(predictions[i]) * 100
            
            # 获取真实标签
            true_class = "猫" if labels[i].numpy() == 0 else "狗"
            
            plt.title(f"预测: {predicted_class} ({confidence:.1f}%)\n真实: {true_class}")
            plt.axis("off")
    plt.tight_layout()
    plt.show()

print("模型预测示例:")
predict_example(model, ds_test)

print("训练完成! 模型已保存为 'cat_dog_model.h5'")    

这个代码执行时有问题,一直无法从tensorflow_datasets中下载猫狗数据集。

解决数据集问题

我们看看怎么能解决这个问题。

首先确认下tensorflow_datasets的版本

这个版本还算挺高的,应该不是版本的问题吧。

既然网上下载的不行,那么就只能手动下载这个数据集,然后再手动加载了。

从 Kaggle 下载

  • 访问 Dogs vs. Cats
  • 下载 train.ziptest1.zip(约 800MB)
  • 这个网站下载要登录外网,然后还下不下来,不推荐;

从 Hugging Face 下载(备选):

这两个地方下载数据集都挺费劲的,看来这些数据集也是要收费的。

咋弄呢?直接百度搜索kaggle的猫狗数据集,然后找一找。

终于是找到了一份

可以看到,有两份文件,一份是训练集,一份是测试集,不得不说,数据挺大的,800多M。

里面啥样的?打开看看

我们先把这个数据集解压到某个目录下:

训练集里就是一大堆猫狗的图片,一共2.5万张

附:电脑的图片不能预览怎么办?

每张图片都标注了cat或者dog,这就是一种标签的方式,后续程序可以通过读取文件名来知道对应图片的标签。

测试集里也是猫狗的图片,但是没有标签(难道不应该也有吗),都是序号。

猫狗数据集里为什么train里面的图片有标签,但是test1里面的图片没有标签?

在猫狗数据集(如 Kaggle 的经典 "Dogs vs. Cats" 数据集)中,train文件夹的图片有标签,而test1文件夹的图片无标签,这是由数据集的设计目的和机器学习的流程决定的,具体原因如下:

  1. 数据集的核心用途:训练与评估
  • train文件夹(带标签)
    用于模型训练 。机器学习模型需要 "已知答案" 的样本(即标签)来学习规律。例如,train中的图片命名为cat.0.jpgdog.1.jpg,通过文件名就能明确标签(猫 / 狗),模型通过这些样本学习 "猫和狗的视觉特征差异"。
  • test1文件夹(无标签)
    用于模拟真实场景的预测 。在实际应用中,模型面对的是 "未知标签" 的数据(比如用户上传的一张图片,模型需要判断是猫还是狗)。test1的设计正是为了检验模型在 "无标签数据" 上的泛化能力,相当于 "期末考试题",答案(标签)不会提前给出。
  1. 竞赛场景的设计(以 Kaggle 为例)

经典的猫狗数据集源自 Kaggle 竞赛,竞赛的目标是让参赛者训练模型后,对test1中的图片进行预测,然后提交预测结果(如每张图片是猫的概率),由官方通过隐藏的标签来评判模型性能。因此:

  • test1的标签被官方保留,用于公平地排名参赛者的模型效果;
  • 参赛者无法直接获取test1的标签,只能通过模型预测来验证自己的训练成果。
  1. 如何使用test1数据?

如果你不是为了参加竞赛,而是自己做练习,可以:

  • 手动划分验证集 :从train中拆分一部分数据作为 "验证集"(带标签),用于评估模型在训练过程中的效果(类似test但非最终考核);
  • 忽略test1的无标签特性 :如果需要 "有标签的测试集",可以手动给test1的图片打标签(但工作量大),或直接用train拆分出的验证集替代。

总结

  • train带标签:供模型学习 "输入(图片)与输出(标签)的关系";
  • test1无标签:模拟真实预测场景,检验模型对未知数据的判断能力,符合机器学习 "训练 - 预测" 的完整流程。

这种设计是机器学习数据集的常见模式,目的是更贴近实际应用场景,避免模型 "记住答案" 而非 "学习规律"。

由此可见,训练和验证阶段,只需要用到train这个目录即可,test1这个目录可用可不用,后续测试也可以自己去找一些图片来进行。

我们就先使用train这个目录

那么,如何加载这个本地数据呢?

由于手动下载的数据集结构与 tensorflow_datasets 期望的不同,我们要用 tf.keras.preprocessing 加载数据:

python 复制代码
import tensorflow as tf
from tensorflow.keras import layers, models, callbacks
import matplotlib.pyplot as plt
import os

# 设置中文显示
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]

# 定义常量
IMAGE_SIZE = (150, 150)
BATCH_SIZE = 32
EPOCHS = 15
NUM_CLASSES = 2

# 数据路径(根据实际修改)
data_dir = r"C:\Users\admin\Desktop\cat_dog_datasets\train"

# 创建数据加载器
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
    data_dir,
    validation_split=0.2,
    subset="training",
    seed=123,
    image_size=IMAGE_SIZE,
    batch_size=BATCH_SIZE,
    label_mode='binary'  # 二分类问题
)

val_ds = tf.keras.preprocessing.image_dataset_from_directory(
    data_dir,
    validation_split=0.2,
    subset="validation",
    seed=123,
    image_size=IMAGE_SIZE,
    batch_size=BATCH_SIZE,
    label_mode='binary'
)

# 数据增强(可选)
data_augmentation = tf.keras.Sequential([
    layers.RandomFlip('horizontal'),
    layers.RandomRotation(0.2),
])

# 构建模型
def build_model():
    model = models.Sequential([
        data_augmentation,  # 数据增强层
        
        layers.Rescaling(1./255),  # 归一化
        
        layers.Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3)),
        layers.MaxPooling2D((2, 2)),
        layers.Conv2D(64, (3, 3), activation='relu'),
        layers.MaxPooling2D((2, 2)),
        layers.Conv2D(128, (3, 3), activation='relu'),
        layers.MaxPooling2D((2, 2)),
        
        layers.Flatten(),
        layers.Dense(128, activation='relu'),
        layers.Dropout(0.5),
        layers.Dense(1, activation='sigmoid')  # 二分类问题用sigmoid
    ])
    
    model.compile(
        optimizer='adam',
        loss='binary_crossentropy',  # 二分类损失函数
        metrics=['accuracy']
    )
    
    return model

# 其余代码(训练、评估、可视化)保持不变...

运行时依然报错

错误描述如下:

python 复制代码
ValueError: When passing `label_mode="binary"`, there must be exactly 2 class_names. Received: class_names=[]

根据搜索和测试,发现上面的这种加载数据的方式,是识别目录名为类别的,也就是说,本来需要有cat和dog这两个目录,但是现在train里面没有这两个目录,所以没有标签,于是,就在train里面将猫和狗分成两个子目录存放。

这样操作之后重新加载数据OK了,可以看到,20000用来训练,5000用来验证。

处理模型构建的问题

又遇到问题了

python 复制代码
ValueError: This model has not yet been built. Build the model first by calling `build()` or by calling the model on a batch of data.

这个错误 ValueError: This model has not yet been built 通常在你尝试在模型未完全构建前访问其属性或方法时出现。在 TensorFlow 中,模型需要通过输入数据来推断各层的形状(shape),才能完成构建。以下是详细解释和解决方案:

问题原因

在 TensorFlow 中,模型的构建分为两个阶段:

  1. 定义结构 :通过 Sequential() 或函数式 API 定义网络层。
  2. 构建模型 :通过输入数据(或手动调用 build())让模型确定各层的输入输出形状。

如果你在模型构建前(例如调用 model.summary() 或保存模型时)访问模型属性,就会触发此错误。

常见触发场景

  1. 直接调用 model.summary():在未输入任何数据前调用。
  2. 保存空模型:尝试保存未训练的模型。
  3. 访问模型参数 :如 model.layersmodel.weights

解决方案

1. 先让模型处理一批数据

在调用 model.summary() 或其他需要模型构建的操作前,先给模型喂一批数据:

复制代码
model = build_model()  # 定义模型

# 构建模型:通过输入一批数据让模型推断形状
dummy_input = tf.random.normal((1, 150, 150, 3))  # 批次大小1,图像尺寸150x150
model(dummy_input)  # 关键:让模型处理数据

# 现在可以安全地调用summary()
model.summary()

2. 手动调用 build() 方法(仅适用于 Sequential 模型)

如果你使用 Sequential 模型,可以手动指定输入形状:

复制代码
model = build_model()

# 手动构建模型,指定输入形状
model.build(input_shape=(None, 150, 150, 3))  # None表示批次大小可变

# 现在可以调用summary()
model.summary()

这里我们选择第一种方案。

解决之后,最终的程序如下:

python 复制代码
import tensorflow as tf
from tensorflow.keras import layers, models, callbacks
import matplotlib.pyplot as plt
import os

# 设置中文显示
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]

# 定义常量
IMAGE_SIZE = (150, 150)
BATCH_SIZE = 32
EPOCHS = 15

# 数据路径 - 修改为你解压数据集的路径
data_dir = r"C:\Users\admin\Desktop\cat_dog\train"

# 检查数据目录是否存在
if not os.path.exists(data_dir):
    print(f"错误: 数据目录 '{data_dir}' 不存在!")
    print("请确保已将数据集解压到正确路径,目录结构应为:")
    print(r"C:\datasets\cats_vs_dogs\train\cat.0.jpg")
    print(r"C:\datasets\cats_vs_dogs\train\dog.0.jpg")
    exit(1)

# 创建数据加载器
print("正在加载数据集...")
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
    data_dir,
    validation_split=0.2,
    subset="training",
    seed=123,
    image_size=IMAGE_SIZE,
    batch_size=BATCH_SIZE,
    label_mode='binary'  # 二分类问题
)

val_ds = tf.keras.preprocessing.image_dataset_from_directory(
    data_dir,
    validation_split=0.2,
    subset="validation",
    seed=123,
    image_size=IMAGE_SIZE,
    batch_size=BATCH_SIZE,
    label_mode='binary'
)

# 数据增强
data_augmentation = tf.keras.Sequential([
    layers.RandomFlip('horizontal'),
    layers.RandomRotation(0.2),
    layers.RandomZoom(0.2),
])

# 构建模型
def build_model():
    model = models.Sequential([
        data_augmentation,
        
        # 归一化
        layers.Rescaling(1./255),
        
        # 卷积层
        layers.Conv2D(32, (3, 3), activation='relu', padding='same'),
        layers.MaxPooling2D((2, 2)),
        layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
        layers.MaxPooling2D((2, 2)),
        layers.Conv2D(128, (3, 3), activation='relu', padding='same'),
        layers.MaxPooling2D((2, 2)),
        
        # 全连接层
        layers.Flatten(),
        layers.Dense(128, activation='relu'),
        layers.Dropout(0.5),  # 防止过拟合
        layers.Dense(1, activation='sigmoid')  # 二分类问题
    ])
    
    # 编译模型
    model.compile(
        optimizer='adam',
        loss='binary_crossentropy',
        metrics=['accuracy']
    )
    
    return model

# 创建模型
model = build_model()

# 构建模型:通过输入数据推断形状
dummy_input = tf.random.normal((1, 150, 150, 3))
model(dummy_input)

# 现在可以安全地查看模型结构
print("模型结构:")
model.summary()

# 可视化样本
def visualize_samples(dataset):
    plt.figure(figsize=(10, 10))
    for images, labels in dataset.take(1):
        for i in range(9):
            ax = plt.subplot(3, 3, i + 1)
            plt.imshow(images[i].numpy().astype("uint8"))
            plt.title("猫" if labels[i].numpy() == 0 else "狗")
            plt.axis("off")
    plt.tight_layout()
    plt.show()

print("数据样本可视化:")
visualize_samples(train_ds)

# 设置回调函数
callbacks_list = [
    callbacks.EarlyStopping(
        monitor='val_loss',
        patience=3,
        restore_best_weights=True
    ),
    callbacks.ModelCheckpoint(
        filepath='cat_dog_model.h5',
        monitor='val_accuracy',
        save_best_only=True,
        verbose=1
    ),
    callbacks.ReduceLROnPlateau(
        monitor='val_loss',
        factor=0.2,
        patience=2,
        min_lr=0.00001
    )
]

# 训练模型
print("开始训练模型...")
history = model.fit(
    train_ds,
    epochs=EPOCHS,
    validation_data=val_ds,
    callbacks=callbacks_list
)

# 评估模型
print("在验证集上评估模型...")
val_loss, val_acc = model.evaluate(val_ds)
print(f"验证集准确率: {val_acc:.4f}")

# 绘制训练历史
def plot_training_history(history):
    plt.figure(figsize=(12, 4))
    
    # 绘制准确率曲线
    plt.subplot(1, 2, 1)
    plt.plot(history.history['accuracy'], label='训练准确率')
    plt.plot(history.history['val_accuracy'], label='验证准确率')
    plt.xlabel('Epoch')
    plt.ylabel('准确率')
    plt.legend()
    plt.title('训练和验证准确率')
    
    # 绘制损失曲线
    plt.subplot(1, 2, 2)
    plt.plot(history.history['loss'], label='训练损失')
    plt.plot(history.history['val_loss'], label='验证损失')
    plt.xlabel('Epoch')
    plt.ylabel('损失')
    plt.legend()
    plt.title('训练和验证损失')
    
    plt.tight_layout()
    plt.show()

plot_training_history(history)

# 预测示例
def predict_example(model, dataset):
    plt.figure(figsize=(12, 12))
    for images, labels in dataset.take(1):
        predictions = model.predict(images)
        for i in range(9):
            ax = plt.subplot(3, 3, i + 1)
            plt.imshow(images[i].numpy().astype("uint8"))
            
            # 获取预测结果
            predicted_class = "猫" if predictions[i][0] < 0.5 else "狗"
            confidence = (1 - predictions[i][0]) * 100 if predicted_class == "猫" else predictions[i][0] * 100
            
            # 获取真实标签
            true_class = "猫" if labels[i].numpy() == 0 else "狗"
            
            plt.title(f"预测: {predicted_class} ({confidence:.1f}%)\n真实: {true_class}")
            plt.axis("off")
    plt.tight_layout()
    plt.show()

print("模型预测示例:")
predict_example(model, val_ds)

print("训练完成! 模型已保存为 'cat_dog_model.h5'")

我们可以直接执行这个程序了。

程序解读

程序能执行了,但是我们先不着急操作,为了后续能看懂执行过程和结果,我们先来解读下程序,看看都啥意思。

1、导入必要的模块

1、导入必要的模块

主要是导入tensorflow和keras,以及其他需要的库比如可视化matplotlib等等,按需导入即可

2、防止中文显示乱码

2,可选,设置matplotlib的中文显示,防止显示时的乱码

rcParams 是 Matplotlib 库中用于配置全局绘图参数的重要工具。通过修改 rcParams,你可以自定义图形的样式、字体、颜色、尺寸等属性,使所有图表保持一致的风格。

rcParams 是一个类似字典的对象,存储了 Matplotlib 的默认配置。你可以通过以下方式访问和修改它:

python 复制代码
import matplotlib.pyplot as plt

# 查看当前配置
print(plt.rcParams)

# 修改单个参数(示例:设置中文字体)
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]

# 修改多个参数
plt.rcParams.update({
    "figure.figsize": (10, 6),  # 图表大小
    "axes.grid": True,          # 显示网格线
    "lines.linewidth": 2,       # 线条宽度
    "font.size": 12             # 字体大小
})

我们这里用到的是字体设置

python 复制代码
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]

为啥这里在列表指定了三种字体?

Matplotlib 会按顺序尝试列表中的字体,直到找到可用的字体,是为了防止有些字体不存在,如果所有指定字体都不存在,Matplotlib 会使用系统默认字体(可能无法正确显示中文)。

找不到的提示:

如果你的代码只在特定操作系统(如 Windows)上运行,且确保该系统预装了指定字体(如SimHei),也可以只指定一种字体,比如:

python 复制代码
plt.rcParams["font.family"] = "SimHei" # 只指定一种字体

即使你认为环境可控,仍建议使用回退策略(指定多个字体),以提高代码的健壮性。

3,定义必要的常量

3,定义必要的常量

在深度学习中,IMAGE_SIZEBATCH_SIZEEPOCHS 是三个核心超参数,控制着模型训练的流程和效率。以下是它们的具体含义和作用:


IMAGE_SIZE(图像尺寸)

  • 含义:将输入图像统一调整为的尺寸(宽 × 高)。

  • 作用

    • 所有输入图像必须具有相同尺寸,因为深度学习模型的输入层维度是固定的。
    • 较大的尺寸保留更多细节,但增加计算量;较小的尺寸提高速度,但可能丢失关键信息。
  • 示例

    复制代码
    IMAGE_SIZE = (150, 150)  # 图像将被调整为150×150像素
  • 代码影响

    复制代码
    # 数据加载时调整图像尺寸
    train_ds = tf.keras.preprocessing.image_dataset_from_directory(
        data_dir,
        image_size=IMAGE_SIZE,  # 关键参数
        ...
    )
  • 在将图片调整为统一尺寸时,缩放裁剪是两种常见方式,它们各有优缺点,且都会在一定程度上改变原始图像的呈现 ------ 关键是根据场景选择更适合的方式,以减少对关键信息的损失。

  • 缩放和裁剪都会改变原始图像,没有完美的方案,但可通过策略减少关键信息损失。

  • 核心原则:让处理方式匹配图像的主体位置------ 主体居中用裁剪,边缘重要用缩放 + 填充,精准场景用智能裁剪。

  • 实际开发中,可尝试两种方式并对比模型效果(如准确率),选择更适合当前数据集的方案。


BATCH_SIZE(批次大小)

  • 含义:每次训练时同时输入模型的样本数量。

  • 作用

    • 内存优化:GPU 内存有限,无法同时处理所有数据,因此将数据分成小批次。
    • 梯度稳定性:批次越大,梯度估计越稳定,但可能陷入局部最优;批次越小,随机性越强,可能帮助跳出局部最优。
  • 示例

    复制代码
    BATCH_SIZE = 32  # 每次训练使用32张图像
  • 代码影响

    复制代码
    train_ds = tf.keras.preprocessing.image_dataset_from_directory(
        ...,
        batch_size=BATCH_SIZE,  # 关键参数
    )
  • 选择技巧

    • 较大的批次(如 64、128)适合大规模数据集和强大的 GPU。
    • 较小的批次(如 16、32)适合内存有限的环境或需要更多随机性的场景。
  • 当你设置 BATCH_SIZE = 32 时,模型会同时接收 32 张图片作为输入,计算它们的前向传播和梯度。这不是通过循环逐一处理图片,而是利用硬件的并行计算能力一次性处理。

并行计算的实现层级

Batch 的并行处理主要通过以下两种方式实现:

(1)硬件层面的并行

  • GPU(图形处理器)

    GPU 由数千个小型计算核心组成,适合同时处理大量数据。当输入一个 Batch 时,GPU 会将计算任务分配给不同的核心:

    • 示例:32 张图片的卷积计算会被分割到不同核心,每个核心处理一部分像素或通道,最终汇总结果。
    • 优势:充分利用 GPU 的并行架构,速度比 CPU 快数十倍。
  • CPU

    CPU 也支持多线程并行,但核心数较少(通常 4-16 核),适合小批量或简单计算。TensorFlow 默认会自动利用 CPU 的多线程能力。

(2)框架层面的优化

深度学习框架(如 TensorFlow、PyTorch)会自动优化 Batch 的并行计算:

  • 向量化运算
    将 Batch 中的所有样本打包成一个高维张量(例如 [32, 150, 150, 3] 表示 32 张 150×150 的 RGB 图像),通过一次矩阵运算处理整个 Batch,而非循环处理每个样本。
  • 内存优化
    框架会将 Batch 数据预加载到 GPU 内存中,减少数据传输延迟,提高并行效率。

Batch=32 的处理时间通常不会是 Batch=1 的 32 倍,而是远小于 32 倍(例如 5-10 倍),这证明了并行计算的效率。

注意事项:并非 Batch 越大越好

虽然 Batch 并行能提升效率,但过大的 Batch 可能导致:

  1. 内存溢出:GPU 内存无法容纳整个 Batch。
  2. 梯度稳定性问题:大 Batch 的梯度更新方向更稳定,但可能陷入局部最优。
  3. 训练速度下降:当 Batch 超过 GPU 并行处理能力的上限时,效率反而降低。

EPOCHS(训练轮数)

  • 含义:整个训练数据集被模型 "遍历" 的次数。

  • 作用

    • 模型需要多次遍历数据才能学习到足够的特征。
    • 过多的轮数可能导致过拟合(模型在训练数据上表现好,但泛化能力差)。
  • 示例

    复制代码
    EPOCHS = 15  # 整个数据集将被训练15次
  • 代码影响

    复制代码
    history = model.fit(
        train_ds,
        epochs=EPOCHS,  # 关键参数
        ...
    )
  • 选择技巧

    • 通过观察训练和验证损失曲线来调整:当验证损失开始上升时,停止训练(早停策略)。
    • 复杂任务可能需要更多轮次(如 100+),简单任务可能只需几轮。

三者关系示例

假设你有 1000 张训练图像:

  • BATCH_SIZE = 32:每次训练使用 32 张图像。
  • 每轮(Epoch)需要的步数:1000 ÷ 32 ≈ 32步
  • EPOCHS = 15:总共需要训练 32步/轮 × 15轮 = 480步

总结

参数 含义 影响
IMAGE_SIZE 输入图像尺寸 计算量、模型复杂度、特征保留程度
BATCH_SIZE 每批次样本数 内存使用、训练速度、梯度稳定性
EPOCHS 训练轮数 模型学习深度、过拟合风险

调参建议

  • IMAGE_SIZE:根据模型和数据特性选择,常见值有 128、150、224。
  • BATCH_SIZE:从 32 开始尝试,根据 GPU 内存调整(如内存不足可降至 16)。
  • EPOCHS:使用早停回调(EarlyStopping)自动确定最佳轮数。

4、数据集路径设置

4、定义数据集所在路径,并确认路径是否存在

5、加载数据

5、加载数据

这里通过image_dataset_from_directory接口来加载数据,并将数据分为了训练集和验证集

关于image_dataset_from_directory这个接口,参考:tf.keras.preprocessing.image_dataset_from_directory | TensorFlow v2.16.1

看名字就是"从目录中加载图片数据"

Generates a tf.data.Dataset from image files in a directory.

这段代码使用了 TensorFlow 的image_dataset_from_directory函数从目录结构中加载图像数据,并将其划分为训练集和验证集。以下是对代码的详细解析:

数据加载与预处理流程

  1. 训练集加载

    复制代码
    train_ds = tf.keras.preprocessing.image_dataset_from_directory(
        data_dir,                     # 数据集根目录
        validation_split=0.2,         # 保留20%的数据用于验证
        subset="training",            # 指定加载训练集部分
        seed=123,                     # 随机种子,确保划分可重复
        image_size=IMAGE_SIZE,        # 图像统一调整的尺寸
        batch_size=BATCH_SIZE,        # 每个批次的样本数
        label_mode='binary'           # 二分类标签模式
    )
  2. 验证集加载

    复制代码
    val_ds = tf.keras.preprocessing.image_dataset_from_directory(
        data_dir,                     # 与训练集相同的根目录
        validation_split=0.2,         # 保持相同的划分比例
        subset="validation",          # 指定加载验证集部分
        seed=123,                     # 必须与训练集相同的种子
        image_size=IMAGE_SIZE,        # 保持相同的图像尺寸
        batch_size=BATCH_SIZE,        # 批次大小
        label_mode='binary'           # 二分类标签模式
    )

参数解析

  • data_dir:数据集根目录,目录结构应为:

复制代码
  data_dir/
  ├── class_a/
  │   ├── image_001.jpg
  │   └── image_002.jpg
  └── class_b/
      ├── image_003.jpg
      └── image_004.jpg

每个子目录名称即为类别标签。

  • validation_split=0.2:将数据集按 8:2 比例划分为训练集和验证集。

  • subset:指定加载 "training" 或 "validation" 子集。

  • seed=123:固定随机种子,确保训练集和验证集的划分一致。

  • image_size=IMAGE_SIZE :所有图像将被调整为指定尺寸(例如(224, 224))。

  • batch_size=BATCH_SIZE:每个批次包含的样本数,影响训练速度和内存使用。

  • label_mode='binary':适用于二分类问题,生成 0/1 标签。

注意事项

  • 目录结构要求:必须严格遵循 "根目录 / 类别 / 图像" 的层次结构。
  • 随机种子一致性 :训练集和验证集必须使用相同的seed,确保数据不重叠
  • 标签编码label_mode='binary'适用于二分类,多分类需使用label_mode='categorical'

此代码片段实现了图像数据的高效加载与划分,为后续模型训练提供了标准化的输入数据流。

6、数据增强

6、数据增强

这段代码使用 TensorFlow 的 Keras API 创建了一个数据增强流水线,通过随机变换增加训练数据的多样性。数据增强是深度学习中常用的正则化技术,尤其在图像数据有限的情况下,可以有效提升模型的泛化能力。

复制代码
data_augmentation = tf.keras.Sequential([
    layers.RandomFlip('horizontal'),      # 随机水平翻转图像
    layers.RandomRotation(0.2),           # 随机旋转图像(±20% * 2π 弧度)
    layers.RandomZoom(0.2),               # 随机缩放图像(±20%)
])

各层功能详解:

  • layers.RandomFlip('horizontal')

    • 作用:以 50% 的概率对图像进行水平翻转。
    • 适用场景:适用于左右对称的图像(如自然场景、人脸),但不适用于有明确方向的图像(如文本、箭头)。
  • layers.RandomRotation(0.2)

    • 作用 :随机旋转图像,范围为 -0.2×2π0.2×2π 弧度(约 ±36°)。
    • 参数含义0.2 表示旋转角度的比例因子,实际角度范围为 [-0.2×2π, 0.2×2π]
  • layers.RandomZoom(0.2)

    • 作用:随机缩放图像,在宽度和高度方向上独立进行缩放。
    • 参数含义0.2 表示缩放范围为原始尺寸的 [1-0.2, 1+0.2] = [0.8, 1.2] 倍。

数据增强层可以在两种场景下使用:

  1. 模型内部(推荐):将增强层作为模型的第一层,在训练时实时处理数据:

    复制代码
    model = tf.keras.Sequential([
        data_augmentation,                # 增强层作为输入预处理
        layers.Rescaling(1./255),         # 归一化
        layers.Conv2D(16, 3, activation='relu'),
        # ... 后续网络层
    ])
  2. 数据集处理:在数据加载后、输入模型前应用增强:

    复制代码
    train_ds = train_ds.map(lambda x, y: (data_augmentation(x, training=True), y))

注意事项

  1. 仅在训练时生效 :数据增强只在训练阶段应用(training=True),推理时自动跳过。

  2. 避免信息丢失 :旋转和缩放可能导致图像边缘信息丢失,需配合 fill_mode 参数(如 'nearest''reflect')处理空白区域。

  3. 组合增强效果:多种增强操作组合使用时,可能产生复杂变换(如翻转后再旋转),需根据数据特性调整顺序和参数。

  4. 性能影响 :实时增强会增加训练计算开销,可通过 dataset.cache() 缓存增强结果(但需确保内存足够)。

  5. 扩展增强选项:还可添加其他增强层,如:

    复制代码
    layers.RandomContrast(0.2),          # 随机调整对比度
    layers.RandomTranslation(0.1, 0.1),  # 随机平移

数据增强通过模拟真实世界中的图像变化,帮助模型学习更鲁棒的特征,尤其适用于医学图像、自动驾驶等数据有限的场景。

7、构建模型

7、构建模型

这段代码定义了一个用于图像二分类的卷积神经网络模型,包含数据增强、特征提取、分类器三个主要部分。以下是详细解析:

**models.Sequential()**创建一个线性堆叠的神经网络模型,允许按顺序添加多个层(如卷积层、全连接层、激活函数等),形成一个完整的模型结构。

各部分功能解析

1. 数据增强层

  • 作用:通过随机翻转、旋转和缩放增加训练数据多样性,提升模型泛化能力。
  • 注意:仅在训练时生效,推理时自动跳过。

2. 归一化层

  • 作用 :将输入图像的像素值从 [0, 255] 缩放到 [0, 1],加速模型收敛。
  • 数学公式output = input / 255

3. 卷积特征提取层

  • 结构 :3 个卷积块,每个包含:
    1. 卷积层(Conv2D) :使用 3×3 卷积核提取空间特征,padding='same' 保持输出尺寸与输入相同。
    2. 激活函数(ReLU) :引入非线性,增强模型表达能力。ReLU(Rectified Linear Unit)是深度学习中最常用的激活函数之一,它为神经网络引入了非线性特性,是许多成功模型(如 ResNet、VGG)的核心组件。
    3. 最大池化层(MaxPooling2D):通过 2×2 窗口下采样,减少参数并捕获主要特征。
  • 参数变化:卷积核数量从 32→64→128,逐步提取更抽象的特征。

4. 分类器层

  • Flatten:将卷积输出的多维张量展平为一维向量。
  • 全连接层(Dense):128 个神经元,进一步处理特征。
  • Dropout(0.5):训练时随机丢弃 50% 神经元,防止过拟合。
  • 输出层 :1 个神经元 + sigmoid 激活函数,输出范围 [0, 1],适用于二分类问题(如猫 / 狗、正 / 负样本)。

模型编译

此模型结构简单但有效,适合初学者理解 CNN 基本原理,也可作为图像二分类任务的基线模型。

8、查看模型结构

8、查看模型结构

model.summary() 是 TensorFlow/Keras 中用于查看模型结构和参数统计信息的重要工具,它能帮助你快速了解模型的整体架构、每一层的输入输出形状以及参数数量。

作用解析

1. 显示模型架构

打印模型的层次结构,包括每一层的名称、类型、输出形状和参数数量。

2. 统计参数总量

计算模型的总参数数量、可训练参数数量和不可训练参数数量(如 BatchNormalization 中的均值 / 方差)。

3. 检查输入输出形状

确认每一层的输入输出维度是否符合预期,帮助调试模型结构。

示例

假设有以下简单模型:

复制代码
from tensorflow.keras import models, layers

model = models.Sequential([
    layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),
    layers.MaxPooling2D((2, 2)),
    layers.Flatten(),
    layers.Dense(10, activation='softmax')
])

model.summary()

输出结果

复制代码
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d (Conv2D)             (None, 26, 26, 32)        320       
                                                                 
 max_pooling2d (MaxPooling2D  (None, 13, 13, 32)       0         
 )                                                               
                                                                 
 flatten (Flatten)           (None, 5408)              0         
                                                                 
 dense (Dense)               (None, 10)                54090     
                                                                 
=================================================================
Total params: 54,410
Trainable params: 54,410
Non-trainable params: 0
_________________________________________________________________

输出解读

  • 层信息

    • Layer (type) :层的名称和类型(如 conv2d (Conv2D))。
    • Output Shape :输出张量的形状(None 表示批量大小可变)。
    • Param #:该层的参数数量。
  • 参数计算

    • 卷积层(kernel_size × input_channels + 1) × output_channels
      例如:(3×3×1 + 1)×32 = 320
    • 全连接层(input_units + 1) × output_units
      例如:(5408 + 1)×10 = 54090
  • 总参数量

    • Trainable params:可训练的参数(如权重、偏置)。
    • Non-trainable params:不可训练的参数(如 BatchNormalization 的均值 / 方差)。

实际用途

  • 模型调试

    • 检查层的连接是否正确(如输入输出形状是否匹配)。
    • 发现意外的参数数量(如忘记使用 padding='same' 导致特征图尺寸骤减)。
  • 资源评估

    • 参数过多的模型可能导致过拟合或内存不足(如 GPU 显存溢出)。
    • 轻量级模型(如 MobileNet)通常参数较少,适合移动设备。
  • 文档记录

    • 作为模型设计的参考文档,便于团队协作和复现。

注意事项

  • 动态模型 :如果模型包含动态操作(如 Lambda 层或自定义层),summary() 可能无法显示完整信息。
  • 多输入 / 输出模型 :使用 tf.keras.utils.plot_model() 可生成更直观的图形化表示。

输出的形状啥意思?

model.summary() 的输出中,Output Shape 描述了每一层输出张量的维度结构,帮助你理解数据在模型中的流动方式。下面结合具体示例详细解析其含义和计算方式:

基本概念

1. 张量维度表示

  • (None, ...)None 表示该维度的大小可变,通常对应批量大小(batch size)。例如:
    • (None, 224, 224, 3) 表示批量大小不确定,每张图片为 224×224 像素,3 个通道(RGB)。
  • 固定维度 :如 (32,) 表示长度为 32 的向量。

2. 常见层的输出形状计算

层类型 输入形状 输出形状计算方式
Conv2D (None, H, W, C_in) (None, H', W', C_out) 其中: H' = (H + 2×padding - kernel_size)/stride + 1 W' 同理
MaxPooling2D (None, H, W, C) (None, H/stride, W/stride, C)(默认 pool_size=2, stride=2
Flatten (None, H, W, C) (None, H × W × C)
Dense (None, input_units) (None, output_units)

示例解析

假设我们有以下模型:

复制代码
from tensorflow.keras import models, layers

model = models.Sequential([
    layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1), padding='same'),
    layers.MaxPooling2D((2, 2)),
    layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
    layers.MaxPooling2D((2, 2)),
    layers.Flatten(),
    layers.Dense(10, activation='softmax')
])

model.summary()

输出结果

复制代码
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d (Conv2D)             (None, 28, 28, 32)        320       
                                                                 
 max_pooling2d (MaxPooling2D  (None, 14, 14, 32)       0         
 )                                                               
                                                                 
 conv2d_1 (Conv2D)           (None, 14, 14, 64)        18496     
                                                                 
 max_pooling2d_1 (MaxPooling  (None, 7, 7, 64)         0         
 2D)                                                             
                                                                 
 flatten (Flatten)           (None, 3136)              0         
                                                                 
 dense (Dense)               (None, 10)                31370     
                                                                 
=================================================================
Total params: 50,186
Trainable params: 50,186
Non-trainable params: 0
_________________________________________________________________

逐层分析输出形状

1. 输入层

  • 期望输入input_shape=(28, 28, 1)(单通道 28×28 像素图像)
  • 实际形状(None, 28, 28, 1)(批量维度 + 空间维度 + 通道维度)

2. 第一个卷积层 conv2d

  • 参数filters=32, kernel_size=(3, 3), padding='same'
  • 输出形状(None, 28, 28, 32)
    • 计算
      • padding='same' 保证输出尺寸与输入相同(28×28)。
      • 卷积核数量为 32,因此通道数变为 32。

3. 第一个池化层 max_pooling2d

  • 参数pool_size=(2, 2), stride=2(默认)
  • 输出形状(None, 14, 14, 32)
    • 计算
      • 空间尺寸减半:28 ÷ 2 = 14。
      • 通道数不变(仍为 32)。

4. 第二个卷积层 conv2d_1

  • 参数filters=64, kernel_size=(3, 3), padding='same'
  • 输出形状(None, 14, 14, 64)
    • 计算
      • padding='same' 保持空间尺寸(14×14)。
      • 卷积核数量为 64,通道数变为 64。

5. 第二个池化层 max_pooling2d_1

  • 参数pool_size=(2, 2), stride=2
  • 输出形状(None, 7, 7, 64)
    • 计算
      • 空间尺寸减半:14 ÷ 2 = 7。
      • 通道数不变(仍为 64)。

6. 展平层 flatten

  • 输出形状(None, 3136)
    • 计算
      • 7×7×64 = 3136(将空间维度和通道维度展平为一维向量)。

7. 全连接层 dense

  • 参数units=10(10 个类别)
  • 输出形状(None, 10)
    • 含义:每个样本输出 10 个值,表示属于 10 个类别的概率分布。

关键注意点

  • 批量维度 None

    • 模型在训练 / 推理时可接受任意批量大小的输入。
    • 例如,输入 32 张图片时,实际形状为 (32, 28, 28, 1)
  • 卷积层的 padding 参数

    • padding='valid'(默认):不补零,输出尺寸缩小。
      例如:输入 (28,28) → 3×3 卷积 → 输出 (26,26)。
    • padding='same':补零使输出尺寸与输入相同。
  • 池化层的影响

    • 池化操作通过降采样(如 2×2 池化)减少空间维度,同时保留主要特征。
    • 池化不改变通道数。
  • Flatten 层的作用

    • 将多维张量展平为一维向量,便于全连接层处理。
    • 展平后的维度 = 所有空间维度和通道维度的乘积。

常见问题排查

  • 形状不匹配错误

    • 例如,全连接层期望输入 (None, 100),但实际输入为 (None, 50)
    • 解决方法:调整卷积层 / 池化层参数,或修改全连接层的输入单元数。
  • 意外的内存占用

    • 较大的特征图尺寸(如 (None, 224, 224, 512))会显著增加内存消耗。
    • 优化方法:添加更多池化层或使用步长更大的卷积。

通过理解输出形状,你可以:

  • 验证模型结构是否符合设计预期。
  • 诊断层连接错误(如形状不匹配)。
  • 优化模型参数(如减少不必要的特征图尺寸)。

9、模型训练

9、模型训练

上面我们处理好了数据,然后构建好了模型,接下来就要开始训练了。

这段代码使用 model.fit() 方法训练深度学习模型,下面详细解析其功能和参数:

核心功能

model.fit() 是 Keras 中最常用的模型训练方法,它会:

  1. 迭代训练数据 :按批次(batch)从 train_ds 中获取数据。
  2. 前向传播:将输入数据传入模型,得到预测结果。
  3. 计算损失 :根据配置的损失函数(如 binary_crossentropy)计算预测与真实标签的差异。
  4. 反向传播 :使用优化器(如 Adam)根据损失梯度更新模型权重。
  5. 验证评估 :每个 epoch 结束后,在 validation_data 上评估模型性能。

参数解析

复制代码
history = model.fit(
    train_ds,                   # 训练数据集(包含输入和标签)
    epochs=EPOCHS,              # 训练轮数(整个数据集遍历次数)
    validation_data=val_ds,     # 验证数据集(可选)
    callbacks=callbacks_list    # 回调函数列表(可选)
)

关键参数

  • train_ds

    • 类型:tf.data.Dataset 或类似可迭代对象。
    • 作用:提供训练数据,通常包含输入图像和对应标签。
    • 示例:通过 image_dataset_from_directory 生成的数据集。
  • epochs

    • 类型:整数。
    • 作用:指定训练的轮数。1 个 epoch 表示模型完整遍历一次训练集。
    • 注意:过大的 epochs 可能导致过拟合,需配合早停(EarlyStopping)。
  • validation_data

    • 类型:与 train_ds 类似的验证数据集。
    • 作用:每个 epoch 结束后,在验证集上评估模型性能,监控泛化能力。
  • callbacks

    • 类型:回调函数列表。

    • 作用:在训练过程中执行特定操作(如保存模型、调整学习率)。

    • 常见回调:

      python 复制代码
      callbacks_list = [
          tf.keras.callbacks.EarlyStopping(      # 早停:当验证指标不再提升时停止训练
              patience=3,                        # 容忍验证集性能下降的最大轮数
              restore_best_weights=True          # 恢复最佳性能的模型权重
          ),
          tf.keras.callbacks.ModelCheckpoint(    # 模型检查点:定期保存模型
              'best_model.h5',                   # 保存路径,默认保存在当前工作目录下(即运行脚本的目录)。
              monitor='val_accuracy',            # 监控指标
              save_best_only=True                # 只保存性能最好的模型
          ),
          tf.keras.callbacks.ReduceLROnPlateau(  # 学习率调度:性能停滞时降低学习率
              factor=0.5,                        # 学习率降低因子
              patience=2                         # 容忍性能不提升的轮数
          )
      ]

返回值 history

  • 类型:History 对象,包含训练过程的详细记录。

  • 主要属性:

    复制代码
    history.history = {
        'loss': [训练损失值列表],          # 每个 epoch 的训练损失
        'accuracy': [训练准确率列表],      # 每个 epoch 的训练准确率(若有)
        'val_loss': [验证损失值列表],      # 每个 epoch 的验证损失
        'val_accuracy': [验证准确率列表]   # 每个 epoch 的验证准确率(若有)
    }
  • 用途:可视化训练过程(如绘制损失曲线、准确率曲线)。

model.fit() 是模型训练的核心接口,通过合理配置参数(如 epochscallbacks)和监控 history,可以高效训练出性能良好的模型。

10、评估模型

10、评估模型

这段代码使用 model.evaluate() 方法在验证集上评估模型的性能,下面详细解析其功能和使用场景:

核心功能

model.evaluate() 的主要作用是:

  1. 批量处理数据 :按批次从验证集 val_ds 中获取数据。
  2. 计算预测结果:将输入数据传入模型,得到预测值。
  3. 评估指标 :根据模型编译时指定的损失函数和评估指标(如 accuracy),计算整体性能。
  4. 返回结果:返回平均损失值和指定的评估指标值。

参数解析

复制代码
val_loss, val_acc = model.evaluate(
    val_ds,                 # 验证数据集
    batch_size=None,        # 批量大小(默认使用数据集的原有设置)
    verbose=1               # 日志显示模式(0=静默,1=进度条,2=每轮一行)
)

关键参数

  • val_ds

    • 类型:tf.data.Dataset 或类似可迭代对象。
    • 作用:提供验证数据,通常包含输入图像和对应标签。
    • 示例:通过 image_dataset_from_directory 生成的验证集。
  • batch_size

    • 类型:整数或 None
    • 作用:指定评估时的批量大小。若为 None,则使用数据集的原有设置。
    • 注意:需根据 GPU 内存调整,避免溢出。
  • verbose

    • 类型:整数(0、1、2)。
    • 作用:控制评估过程的日志显示方式:
      • 0:不显示任何信息。
      • 1:显示进度条(默认值)。
      • 2:每个 epoch 显示一行信息。

返回值

  • 单输出模型

    返回一个列表 [loss, metric1, metric2, ...],例如:

    复制代码
    val_loss, val_acc = model.evaluate(val_ds)
  • 多输出模型

    返回字典或列表,需根据模型结构解析,例如:

    复制代码
    results = model.evaluate(val_ds)
    # 假设模型有两个输出,编译时指定了 loss 和 accuracy
    val_loss = results[0]
    val_acc_output1 = results[1]
    val_acc_output2 = results[2]

model.fit() 中验证的区别

场景 model.evaluate() model.fit(validation_data=val_ds)
执行时机 训练后手动调用 每个 epoch 结束后自动执行
主要用途 最终模型评估 训练过程中监控泛化能力
返回值 单个评估结果 包含所有 epoch 的历史记录
是否影响训练过程

执行过程和结果

执行上面的程序,有如下过程日志

python 复制代码
2025-07-10 11:06:45.575916: I tensorflow/core/util/port.cc:113] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.
WARNING:tensorflow:From C:\Users\admin\AppData\Local\Programs\Python\Python310\lib\site-packages\keras\src\losses.py:2976: The name tf.losses.sparse_softmax_cross_entropy is deprecated. Please use tf.compat.v1.losses.sparse_softmax_cross_entropy instead.

正在加载数据集...
Found 25000 files belonging to 2 classes.
Using 20000 files for training.
2025-07-10 11:06:51.795921: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: SSE SSE2 SSE3 SSE4.1 SSE4.2 AVX2 AVX_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
Found 25000 files belonging to 2 classes.
Using 5000 files for validation.
WARNING:tensorflow:From C:\Users\admin\AppData\Local\Programs\Python\Python310\lib\site-packages\keras\src\backend.py:873: The name tf.get_default_graph is deprecated. Please use tf.compat.v1.get_default_graph instead.

WARNING:tensorflow:From C:\Users\admin\AppData\Local\Programs\Python\Python310\lib\site-packages\keras\src\layers\pooling\max_pooling2d.py:161: The name tf.nn.max_pool is deprecated. Please use tf.nn.max_pool2d instead.

WARNING:tensorflow:From C:\Users\admin\AppData\Local\Programs\Python\Python310\lib\site-packages\keras\src\optimizers\__init__.py:309: The name tf.train.Optimizer is deprecated. Please use tf.compat.v1.train.Optimizer instead.

模型结构:
Model: "sequential_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #
=================================================================
 sequential (Sequential)     (1, 150, 150, 3)          0

 rescaling (Rescaling)       (1, 150, 150, 3)          0

 conv2d (Conv2D)             (1, 150, 150, 32)         896

 max_pooling2d (MaxPooling2  (1, 75, 75, 32)           0
 D)

 conv2d_1 (Conv2D)           (1, 75, 75, 64)           18496

 max_pooling2d_1 (MaxPoolin  (1, 37, 37, 64)           0
 g2D)

 conv2d_2 (Conv2D)           (1, 37, 37, 128)          73856

 max_pooling2d_2 (MaxPoolin  (1, 18, 18, 128)          0
 g2D)

 flatten (Flatten)           (1, 41472)                0

 dense (Dense)               (1, 128)                  5308544

 dropout (Dropout)           (1, 128)                  0

 dense_1 (Dense)             (1, 1)                    129

=================================================================
Total params: 5401921 (20.61 MB)
Trainable params: 5401921 (20.61 MB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________
数据样本可视化:
开始训练模型...
Epoch 1/15
WARNING:tensorflow:From C:\Users\admin\AppData\Local\Programs\Python\Python310\lib\site-packages\keras\src\utils\tf_utils.py:492: The name tf.ragged.RaggedTensorValue is deprecated. Please use tf.compat.v1.ragged.RaggedTensorValue instead.

WARNING:tensorflow:From C:\Users\admin\AppData\Local\Programs\Python\Python310\lib\site-packages\keras\src\engine\base_layer_utils.py:384: The name tf.executing_eagerly_outside_functions is deprecated. Please use tf.compat.v1.executing_eagerly_outside_functions instead.

625/625 [==============================] - ETA: 0s - loss: 0.6551 - accuracy: 0.6087   
Epoch 1: val_accuracy improved from -inf to 0.70480, saving model to cat_dog_model.h5
C:\Users\admin\AppData\Local\Programs\Python\Python310\lib\site-packages\keras\src\engine\training.py:3103: UserWarning: You are saving your model as an HDF5 file via `model.save()`. This file format is considered legacy. We recommend using instead the native Keras format, e.g. `model.save('my_model.keras')`.
  saving_api.save_model(
625/625 [==============================] - 76s 119ms/step - loss: 0.6551 - accuracy: 0.6087 - val_loss: 0.5729 - val_accuracy: 0.7048 - lr: 0.0010
Epoch 2/15
625/625 [==============================] - ETA: 0s - loss: 0.6008 - accuracy: 0.6787  
Epoch 2: val_accuracy improved from 0.70480 to 0.72960, saving model to cat_dog_model.h5
625/625 [==============================] - 100s 161ms/step - loss: 0.6008 - accuracy: 0.6787 - val_loss: 0.5377 - val_accuracy: 0.7296 - lr: 0.0010
Epoch 3/15
625/625 [==============================] - ETA: 0s - loss: 0.5733 - accuracy: 0.7018  
Epoch 3: val_accuracy improved from 0.72960 to 0.73460, saving model to cat_dog_model.h5
625/625 [==============================] - 76s 121ms/step - loss: 0.5733 - accuracy: 0.7018 - val_loss: 0.5409 - val_accuracy: 0.7346 - lr: 0.0010
Epoch 4/15
625/625 [==============================] - ETA: 0s - loss: 0.5541 - accuracy: 0.7177  
Epoch 4: val_accuracy improved from 0.73460 to 0.73600, saving model to cat_dog_model.h5
625/625 [==============================] - 75s 120ms/step - loss: 0.5541 - accuracy: 0.7177 - val_loss: 0.5313 - val_accuracy: 0.7360 - lr: 0.0010
Epoch 5/15
625/625 [==============================] - ETA: 0s - loss: 0.5296 - accuracy: 0.7360  
Epoch 5: val_accuracy improved from 0.73600 to 0.76960, saving model to cat_dog_model.h5
625/625 [==============================] - 76s 121ms/step - loss: 0.5296 - accuracy: 0.7360 - val_loss: 0.4685 - val_accuracy: 0.7696 - lr: 0.0010
Epoch 6/15
625/625 [==============================] - ETA: 0s - loss: 0.5090 - accuracy: 0.7517  
Epoch 6: val_accuracy improved from 0.76960 to 0.78220, saving model to cat_dog_model.h5
625/625 [==============================] - 75s 120ms/step - loss: 0.5090 - accuracy: 0.7517 - val_loss: 0.4561 - val_accuracy: 0.7822 - lr: 0.0010
Epoch 7/15
625/625 [==============================] - ETA: 0s - loss: 0.4927 - accuracy: 0.7645  
Epoch 7: val_accuracy did not improve from 0.78220
625/625 [==============================] - 76s 121ms/step - loss: 0.4927 - accuracy: 0.7645 - val_loss: 0.4759 - val_accuracy: 0.7732 - lr: 0.0010
Epoch 8/15
625/625 [==============================] - ETA: 0s - loss: 0.4756 - accuracy: 0.7749  
Epoch 8: val_accuracy improved from 0.78220 to 0.78500, saving model to cat_dog_model.h5
625/625 [==============================] - 75s 121ms/step - loss: 0.4756 - accuracy: 0.7749 - val_loss: 0.4481 - val_accuracy: 0.7850 - lr: 0.0010
Epoch 9/15
625/625 [==============================] - ETA: 0s - loss: 0.4615 - accuracy: 0.7822  
Epoch 9: val_accuracy improved from 0.78500 to 0.81540, saving model to cat_dog_model.h5
625/625 [==============================] - 75s 120ms/step - loss: 0.4615 - accuracy: 0.7822 - val_loss: 0.4063 - val_accuracy: 0.8154 - lr: 0.0010
Epoch 10/15
625/625 [==============================] - ETA: 0s - loss: 0.4490 - accuracy: 0.7888  
Epoch 10: val_accuracy did not improve from 0.81540
625/625 [==============================] - 85s 135ms/step - loss: 0.4490 - accuracy: 0.7888 - val_loss: 0.4098 - val_accuracy: 0.8110 - lr: 0.0010
Epoch 11/15
625/625 [==============================] - ETA: 0s - loss: 0.4366 - accuracy: 0.7990  
Epoch 11: val_accuracy did not improve from 0.81540
625/625 [==============================] - 76s 122ms/step - loss: 0.4366 - accuracy: 0.7990 - val_loss: 0.4241 - val_accuracy: 0.8080 - lr: 0.0010
Epoch 12/15
625/625 [==============================] - ETA: 0s - loss: 0.4027 - accuracy: 0.8181  
Epoch 12: val_accuracy improved from 0.81540 to 0.82660, saving model to cat_dog_model.h5
625/625 [==============================] - 76s 121ms/step - loss: 0.4027 - accuracy: 0.8181 - val_loss: 0.3826 - val_accuracy: 0.8266 - lr: 2.0000e-04
Epoch 13/15
625/625 [==============================] - ETA: 0s - loss: 0.3900 - accuracy: 0.8260  
Epoch 13: val_accuracy did not improve from 0.82660
625/625 [==============================] - 75s 120ms/step - loss: 0.3900 - accuracy: 0.8260 - val_loss: 0.3808 - val_accuracy: 0.8236 - lr: 2.0000e-04
Epoch 14/15
625/625 [==============================] - ETA: 0s - loss: 0.3818 - accuracy: 0.8314  
Epoch 14: val_accuracy did not improve from 0.82660
625/625 [==============================] - 76s 122ms/step - loss: 0.3818 - accuracy: 0.8314 - val_loss: 0.3919 - val_accuracy: 0.8250 - lr: 2.0000e-04
Epoch 15/15
625/625 [==============================] - ETA: 0s - loss: 0.3811 - accuracy: 0.8303  
Epoch 15: val_accuracy improved from 0.82660 to 0.83400, saving model to cat_dog_model.h5
625/625 [==============================] - 76s 122ms/step - loss: 0.3811 - accuracy: 0.8303 - val_loss: 0.3635 - val_accuracy: 0.8340 - lr: 2.0000e-04
在验证集上评估模型...
157/157 [==============================] - 5s 31ms/step - loss: 0.3635 - accuracy: 0.8340
验证集准确率: 0.8340
训练完成! 模型已保存为 'cat_dog_model.h5'

还有几个可视化图:

可以看到,准确率不断上升,损失函数逐渐减小。

我们再来看看保存的模型:

大小有60M+

目标预测

之前是训练和验证模型,然后将训练好的模型保存好,接下来,我们就用已经训练好的模型来进行预测。

以下是一个完整的 Python 脚本,可以直接加载训练好的猫狗分类模型并对新图像进行预测。脚本包含模型加载、图像预处理和预测功能,可直接运行:

python 复制代码
import tensorflow as tf
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt

# 设置中文显示
plt.rcParams["font.family"] = "SimHei"

def load_model(model_path):
    """加载已训练好的模型"""
    try:
        model = tf.keras.models.load_model(model_path)
        print(f"模型已成功加载: {model_path}")
        return model
    except Exception as e:
        print(f"加载模型时出错: {e}")
        return None

def preprocess_image(image_path, target_size=(150, 150)):
    """预处理输入图像"""
    try:
        # 打开图像
        img = Image.open(image_path)
        # 调整大小,保持和训练时一致
        img = img.resize(target_size)
        # 转换为numpy数组
        img_array = np.array(img)
        # 添加批次维度
        img_array = np.expand_dims(img_array, axis=0)
        # 归一化(如果模型训练时使用了Rescaling层,这里不需要手动归一化)
        # img_array = img_array / 255.0
        return img_array, img
    except Exception as e:
        print(f"处理图像时出错: {e}")
        return None, None

def predict_image(model, img_array, class_names=['猫', '狗']):
    """对图像进行预测"""
    if model is None or img_array is None:
        return None, None
    
    # 进行预测
    predictions = model.predict(img_array)
    
    # 对于二分类问题,获取sigmoid输出并转换为类别
    if predictions.shape[1] == 1:  # sigmoid激活
        confidence = predictions[0][0]
        predicted_class = 0 if confidence < 0.5 else 1
        confidence = 1 - confidence if predicted_class == 0 else confidence
    else:  # softmax激活
        confidence = np.max(predictions)
        predicted_class = np.argmax(predictions)
    
    return class_names[predicted_class], confidence

def visualize_prediction(img, class_name, confidence):
    """可视化预测结果"""
    plt.figure(figsize=(6, 4))
    plt.imshow(img)
    plt.axis('off')
    plt.title(f"预测结果: {class_name}\n置信度: {confidence:.2%}")
    plt.tight_layout()
    plt.show()

if __name__ == "__main__":
    # 模型路径(根据实际情况修改)
    model_path = r"C:\Users\admin\Desktop\cat_dog_model.h5"
    
    # 待预测的图像路径(根据实际情况修改)
    image_path = r"C:\Users\admin\Desktop\2.jpg"
    
    # 加载模型
    model = load_model(model_path)
    
    if model:
        # 预处理图像
        img_array, original_img = preprocess_image(image_path)
        
        if img_array is not None:
            # 进行预测
            class_name, confidence = predict_image(model, img_array)
            
            if class_name:
                print(f"预测结果: {class_name}, 置信度: {confidence:.2%}")
                # 可视化结果
                visualize_prediction(original_img, class_name, confidence)

结果如下:

可以看到,置信度还是很高的。

对于猫狗分类模型,如果给一个既不是猫也不是狗的图片,会咋样?试试看

可以看到,存在误识别

  • 原生模型局限性:标准二分类模型无法拒绝未知类别,可能产生无意义的预测。

  • 改进方向:

    • 扩展训练数据,增加 "其他" 类别。

    • 通过置信度阈值或异常检测技术识别未知样本。

    • 使用更高级的开放集识别方法。

在实际应用中,建议结合业务需求选择合适的方案,避免对未知样本的错误分类造成不良影响。

查看模型

直接参考:

Netron的基本使用介绍-CSDN博客