使用CNN实现简单的猫狗分类
完整代码见:基于Keras3.x使用CNN实现简单的猫狗分类,置信度约为:85%

概述

项目整体目录

在这里插入图片描述

  • /data 存放数据集
  • /model 存放训练好的模型
  • /config.py 存储一些关键模型参数和路径信息等
  • /dataset.py 返回数据增强后的数据集,用于模型训练
  • /model.py 定义模型
  • /train.py 训练模型并绘制训练损失和准确度曲线
  • /test.py 测试模型精准度

环境版本

  • python 3.11
  • keras 3.9.2
  • tensorflow 2.19.0

注意

本项目使用Keras3.x实现,代码与keras 2.x有部分不同,请仔细甄别

环境准备

下载miniconda

如果没有下载conda,参照上一篇文章进行下载配置

新建虚拟环境

创建一个用于猫狗识别的虚拟环境,可以指定py版本

conda create --name catanddog python=3.11

基于conda虚拟环境新建Pycharm项目

依次选择菜单路径:
File-NewProject-Pure Python
在弹出的窗口选择:

  • custom environment
  • Select existing
  • Type:conda
  • 选择刚刚新建的catanddog虚拟环境
    如图:
    在这里插入图片描述

下载分类需要用到的依赖

主要用到:keras3.9.2和tensorflow2.19.0

pip install keras

数据准备

从Kaggle上下载常用的猫狗分类数据集,下载下来后,有训练数据(共25000张,猫狗各一半,带标签,命名示例:dog.0.jpg)和测试数据(共12500张,不带标签,命名示例:1.jgp)。
将训练数据分为两份,前20000张用于训练数据,后5000张带标签的数据用于预测模型整体准确度。12500张测试数据可以用于单张图片的模型预测,以及将猫狗分类后放入对应的目录中,方便查看。
如图组织数据:

  • test为12500张不带标签的测试数据;
  • test2为5000张带标签的测试数据;
  • train为20000张训练数据

数据目录结构

注意:必须把训练数据、test2图片放在新建好的cats和dogs目录下,模型才能自动推断标签
在这里插入图片描述

挪动图片可以采用下列代码

import os, shutil
# 将train_dir_tag_cat后2500张猫图像移动到test2_dir_tag_cat
cats = ['cat.{}.jpg'.format(i) for i in range(1000)]
for cat in cats:
    src = os.path.join(train_dir_tag_cat, cat)
    dst = os.path.join(test2_dir_tag_cat, cat)
    shutil.move(src, dst)

config

config.py:用于存储一些关键参数和路径信息等。
训练的batch为:32
训练15个EPOCH

"""
@Author      :Ayaki Shi
@Date        :2025/4/18 11:03 
@Description : 配置信息
"""
import os, shutil

data_dir = './data'

# 训练集、测试集所在路径
test_dir = os.path.join(data_dir, 'test')
test2_dir = os.path.join(data_dir, 'test2')
train_dir = os.path.join(data_dir, 'train')

# 划分标签后的数据路径
train_dir_tag_cat = os.path.join(train_dir, 'cats')
test_dir_tag_cat = os.path.join(test_dir, 'cats')
test2_dir_tag_cat = os.path.join(test2_dir, 'cats')

train_dir_tag_dog = os.path.join(train_dir, 'dogs')
test_dir_tag_dog = os.path.join(test_dir, 'dogs')
test2_dir_tag_dog = os.path.join(test2_dir, 'dogs')

# 训练参数
IMG_SIZE = (256, 256)
BATCH_SIZE = 32
EPOCHS = 15

# 模型路径
MODEL_PATH = './model/CatAndDogClassifier.keras'

准备训练、测试数据集

dataset.py
训练数据经过数据增强后返回,用于测试模型整体准确度的test2无需数据增强直接返回。
注意: 这个方法ImageDataGenerator已经不推荐使用了,因此使用image_dataset_from_directory这个方法,可以根据目录自动推断标签,只是数据增强稍微复杂了点

"""
@Author      :Ayaki Shi
@Date        :2025/4/18 11:02
@Description : 返回dataset
"""

from keras.api.utils import image_dataset_from_directory
from config import train_dir,test2_dir, BATCH_SIZE,IMG_SIZE
from keras import layers, models
import tensorflow as tf

# 数据增强
def create_augmentation_model():
    return models.Sequential([
        layers.RandomFlip("horizontal", seed=42),
        layers.RandomRotation(0.2, fill_mode='nearest', seed=42),
        layers.RandomZoom(0.2, fill_mode='nearest', seed=42),
        layers.RandomContrast(0.3, seed=42),
        layers.RandomTranslation(0.1, 0.1, fill_mode='nearest', seed=42),
    ], name="data_augmentation")

def create_train_dataset():
    train_dataset = image_dataset_from_directory(
        train_dir,
        label_mode = 'binary',
        batch_size = BATCH_SIZE,
        image_size = IMG_SIZE,
        shuffle=True,  # 必须启用 shuffle
        seed=42
    )
    # 创建预处理模型
    augmentation_model = create_augmentation_model()

    # 定义预处理函数
    def preprocess_train(image, label):
        image = augmentation_model(image, training=True)  # 训练模式激活增强
        return image, label

    train_dataset = train_dataset.map(
        preprocess_train,
        num_parallel_calls= tf.data.AUTOTUNE
    )
    print('--------------返回增强后的训练数据集--------------')
    return train_dataset.prefetch(buffer_size=tf.data.AUTOTUNE)


def create_test2_dataset():
    test2_dataset = image_dataset_from_directory(
        test2_dir,
        label_mode = 'binary',
        batch_size = BATCH_SIZE,
        image_size = IMG_SIZE,
        shuffle=False
    )
    print('--------------返回测试数据集[带标签]--------------')
    return test2_dataset

构建模型

model.py 模型结构:

  • 输入层:指定输入数据形状
  • 数据归一化
  • 四层卷积层和四层池化层交替
  • 展平层:将输出的多维特征图展平为一维向量
  • Dropout防止过拟合
  • 两个全连接层,用于特征提取和最终分类
"""
@Author      :Ayaki Shi
@Date        :2025/4/18 11:02 
@Description : 创建模型
"""
from keras import layers, models, optimizers

from config import IMG_SIZE

def create_model():
    model = models.Sequential(
        [
            # 输入层:指定输入数据形状
            layers.Input(shape=(*IMG_SIZE, 3)),
            layers.Rescaling(1./255),  # 归一化到 [0,1]

            # 四层卷积层和四层池化层
            layers.Conv2D(32, (3, 3), activation='relu'),
            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.Conv2D(128, (3, 3), activation='relu'),
            layers.MaxPooling2D(2, 2),

            # 展平层:将输出的多维特征图展平为一维向量
            layers.Flatten(),

            # 防止过拟合
            layers.Dropout(0.5),

            # 两个全连接层,用于特征提取和最终分类
            layers.Dense(512, activation='relu'),
            layers.Dense(1, activation='sigmoid')
        ]
    )


    # 编译模型
    model.compile(loss='binary_crossentropy',  # 损失函数
                  optimizer= optimizers.Adam(learning_rate=1e-4), # 优化器
                  metrics=['accuracy']) # 评估标准:准确率

    print('--------------构建模型成功--------------')
    return model

训练模型

train.py
获取数据集-创建模型-训练模型-保存模型-绘制损失和准确度曲线

"""
@Author      :Ayaki Shi
@Date        :2025/4/18 16:08 
@Description : 训练模型
"""

from dataset import create_train_dataset
from model import create_model
from config import EPOCHS, BATCH_SIZE, MODEL_PATH
import matplotlib.pyplot as plt


def train_model():
    # 获取dataset
    train_dataset = create_train_dataset()

    # 生成模型
    model = create_model()

    # 训练模型
    print('--------------开始训练模型--------------')
    history = model.fit(train_dataset,
              epochs = EPOCHS,
              batch_size = BATCH_SIZE)

    # 保存模型
    print('--------------开始保存模型--------------')
    model.save(MODEL_PATH)

    print('--------------开始绘制损失和准确性曲线--------------')
    # 绘制训练损失曲线
    plt.figure(figsize=(10, 4))
    plt.plot(history.history['loss'], label='Training Loss', color='blue', marker='o')
    plt.title('Training Loss Over Epochs')
    plt.xlabel('Epochs')
    plt.ylabel('Loss')
    plt.legend()
    plt.grid(True)
    plt.show()

    # 绘制训练准确率曲线
    plt.figure(figsize=(10, 4))
    plt.plot(history.history['accuracy'], label='Training Accuracy', color='green', marker='s')
    plt.title('Training Accuracy Over Epochs')
    plt.xlabel('Epochs')
    plt.ylabel('Accuracy')
    plt.legend()
    plt.grid(True)
    plt.show()

if __name__ == '__main__':
    train_model()

训练过程

在这里插入图片描述

损失和准确度曲线

在这里插入图片描述
在这里插入图片描述

测试模型

test.py

使用带标签的测试图片评估整体准确率

代码见后面,可以看到整体准确度为85%左右
在这里插入图片描述

定义模型类

代码见后面

预测单张图片

代码见后面,预测为狗的概率是73%。
在这里插入图片描述

将不带标签的测试图片分类并存到对应目录中

代码见后面,可以看到大部分测试图片都被放到了正确的目录里,但是也有少数错的

在这里插入图片描述
在这里插入图片描述

源码

from keras import models
import numpy as np
import os, shutil
from keras_preprocessing import image
from config import MODEL_PATH,IMG_SIZE,test_dir,test_dir_tag_cat,test_dir_tag_dog

DOG_TAG_STR = 'dog'
CAT_TAG_STR = 'cat'
NUM_IMAGES = 12500             # 测试图片数

class CatAndDogClassifier:
    def __init__(self):
        self.model = models.load_model(MODEL_PATH)
        print("模型加载成功!")

    def predict_single_image(self, img_path):
        img = image.load_img(img_path, target_size=IMG_SIZE)
        img_array = image.img_to_array(img)
        # 错误代码:双重归一化
        # img_array = np.expand_dims(img_array, axis=0) / 255.0
        img_array = np.expand_dims(img_array, axis=0)

        prediction = self.model.predict(img_array)[0][0]
        print(prediction)
        return DOG_TAG_STR if prediction > 0.5 else CAT_TAG_STR, prediction


    def classify_all_images(self):
        # 遍历所有图片
        # for i in range(1, NUM_IMAGES + 1):
        filename = ''
        for i in range(1, NUM_IMAGES + 1):
            try:
                #(文件名为1.jpg到12500.jpg)
                filename = f"{i}.jpg"
                src_path = os.path.join(test_dir, filename)

                # 跳过不存在的文件
                if not os.path.exists(src_path):
                    print(f"Warning: {filename} 不存在,已跳过")
                    continue

                # 进行预测
                label, confidence = self.predict_single_image(src_path)

                # 确定目标目录
                dest_dir = test_dir_tag_dog if label == DOG_TAG_STR else test_dir_tag_cat
                dest_path = os.path.join(dest_dir, filename)

                # 移动文件
                shutil.move(src_path, dest_path)

                if i%500 == 0: # 打印12500行太多了,每500行打印一次
                    print(f"[{i}/12500] {filename} -> {dest_dir} (置信度: {confidence:.2%})")

            except Exception as e:
                print(f"处理 {filename} 时发生错误: {str(e)}")
                continue


def evaluate_model():
    from dataset import create_test2_dataset

    test2_dataset = create_test2_dataset()

    model = models.load_model(MODEL_PATH)
    loss, acc = model.evaluate(test2_dataset)
    print(f'\nTest accuracy: {acc:.2%}')


if __name__ == '__main__':
    # 初始化分类器
    classifier = CatAndDogClassifier()

    # # 评估整体准确率
    # evaluate_model()


    # # 单张图片预测
    # img_path = os.path.join('./data/train/dogs/dog.100.jpg')
    # label, prob = classifier.predict_single_image(img_path)
    # print(f'预测为: {label} (置信度: {prob if label == DOG_TAG_STR else 1 - prob:.2%})')

    # 将不带标签的测试图片分类放入不同的文件夹
    classifier.classify_all_images()

错误记录

双重归一化问题

在预测单张图片过程中,出现了不管什么图片,预测度总是特别低,只有7%左右
在这里插入图片描述
首先预测结果不对第一时间考虑到是不是模型欠拟合或者过拟合的问题。
但是基于以下两个原因:

  • 首先训练过程中记录的准确度和测试整体准确率都是85%,说明模型大概率是没有问题的
  • 其次这个置信度已经低的离谱了
    所以考虑是在测试单张图片对图片处理出现了问题,经过排查,发现问题出在了,我在对单张图片进行了归一化,然后模型中又进行了一次归一化,导致预测置信度极低。
    test.py
    在这里插入图片描述
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐