动物界的福尔摩斯:基于 EfficientNetB6 的高精度100种动物识别
代码详见:https://github.com/xiaozhou-alt/Animals_Recognition
文章目录
一、项目介绍
本项目是一个功能完整的动物识别系统,包含模型训练与交互应用两大部分。系统基于深度学习技术,能够对 100 种不同类别的动物进行准确识别,并通过精心设计的图形用户界面(GUI)提供友好的交互体验,同时包含一个动物认识的小游戏,以及动物图鉴功能。
该系统主要特点包括:
- 采用高效的 EfficientNetB6 模型作为基础架构,结合数据增强和正则化技术,实现高精度动物识别
- 提供三种核心功能:动物图片识别、动物认识小游戏和动物园图鉴
- 支持动物分类浏览(陆地、海洋、空中动物)
- 包含用户进度跟踪,记录解锁动物数量和游戏得分
- 采用现代化 UI 设计,具有动画效果和视觉反馈,提升用户体验
二、文件夹结构
Animals_Recognition/
├── Animal/ # 核心动物图片资源目录(按动物种类分类存储)
├── antelope/ # 羚羊图片子目录
└── ... # 其他动物子目录(共100种动物)
├── README-data.md # 数据相关说明文档
├── README.md
├── assets/ # 静态资源目录
├── class.txt # 动物类别名称文件
├── data.ipynb
├── demo.mp4 # 项目演示视频
├── demo.py # 主应用程序文件
├── log/
├── output/ # 模型与输出结果目录
├── model/ # 模型存储子目录
└── pic/ # 输出图片子目录
├── predict.py # 模型预测脚本
├── test/ # 测试图片目录
├── train.py # 模型训练脚本
└── zoo_icons/ # 动物园图鉴图标目录
三、数据集介绍
本项目使用的动物数据集包含 100 100 100 个不同类别的动物图片,因为使用网页图片提取下载,清洗由我一个人完全进行,数据集数据量较大,所以部分动物文件夹存在 1 % − 1.5 % 1\%-1.5\% 1%−1.5% 的噪声图片,数据集组织结构如下:
- 总类别数: 100 100 100 种动物
- 数据划分:采用 80 % 80\% 80% 作为训练集, 20 % 20\% 20% 作为验证集
- 图片格式: p n g png png
在模型训练过程中,通过数据增强技术扩充了训练样本,包括旋转、平移、缩放、亮度调整等操作,以提高模型的泛化能力
动物的类别信息请查看class.txt
:
antelope
badger
…
woodpecker
zebra
数据集下载:100种动物识别数据集(ScienceDB)
关于数据集的制作和来源请查看:当库里遇上卷积神经网络:基于 EfficientNetV2 的NBA球星分类 的 数据集制作 部分,此文与其使用相同方法制作,只不过图片种类和数量都是翻了一番。。。点个赞,支持一下手艺周吧 orz
四、EfficientNetB6 模型介绍
1. 核心思想:复合模型缩放 (Compound Model Scaling)
E
f
f
i
c
i
e
n
t
N
e
t
EfficientNet
EfficientNet 的核心创新在于提出了一个系统化的 模型缩放方法,它不再像传统方法那样单独缩放网络的深度、宽度或输入图像分辨率,而是通过一个复合系数
ϕ
ϕ
ϕ 来统一缩放这三个维度:
{
深度
:
d
=
α
ϕ
宽度
:
w
=
β
ϕ
分辨率
:
r
=
γ
ϕ
分辨率
:
α
⋅
β
2
⋅
γ
2
≈
2
α
≥
1
,
β
≥
1
,
γ
≥
1
\left\{ \begin{array}{lr} \textbf{深度}:d = \alpha^{\phi} & \\ \textbf{宽度}:w = \beta^{\phi} & \\ \textbf{分辨率}:r = \gamma^{\phi} & \\ \textbf{分辨率}:\alpha \cdot \beta^{2} \cdot \gamma^{2} \approx 2 & \\ \alpha \geq 1,\quad \beta \geq 1,\quad \gamma \geq 1 \end{array} \right.
⎩
⎨
⎧深度:d=αϕ宽度:w=βϕ分辨率:r=γϕ分辨率:α⋅β2⋅γ2≈2α≥1,β≥1,γ≥1
其中
α
α
α,
β
β
β,
γ
γ
γ 是通过神经架构搜索确定的基础缩放系数,
ϕ
ϕ
ϕ 是用户控制的复合系数。对于
E
f
f
i
c
i
e
n
t
N
e
t
−
B
6
EfficientNet-B6
EfficientNet−B6,
ϕ
=
6
ϕ=6
ϕ=6,这意味着它在
B
0
B0
B0 基础版本上进行了大幅度的均衡放大
🤓🤓🤓小周有话说:
想象一下你要设计一个高效的 物流系统:
- 深度(
d
):就像仓库的层数,层数越多能存放的货物越多(特征提取能力越强)- 宽度(
w
):就像每层仓库的货架数量,货架越多能处理的货物越多(同时处理更多特征)- 分辨率(
r
):就像每个货物的检查精细度,检查得越仔细信息越丰富(图像细节越多)传统方法是只增加层数(导致货物上下运输效率低)或只增加货架(导致管理混乱)。 E f f i c i e n t N e t EfficientNet EfficientNet 的巧妙之处在于同时、均衡地扩大这三个方面,就像设计一个既增加层数、又增加每层货架、还提升检查精细度的智能物流系统,让整体效率最大化
2. 基础构建块:MBConv (Mobile Inverted Bottleneck Conv)
E
f
f
i
c
i
e
n
t
N
e
t
−
B
6
EfficientNet-B6
EfficientNet−B6 的基本构建单元是
M
B
C
o
n
v
MBConv
MBConv 模块,其数学表达可以简化为:
MBConv
(
x
)
=
SE
(
DepthwiseConv
(
PointwiseConv
up
(
x
)
)
)
×
ExpansionRatio
+
x
\text{MBConv}(x) = \text{SE}\left( \text{DepthwiseConv}\left( \text{PointwiseConv}_{\text{up}}(x) \right) \right) \times \text{ExpansionRatio} + x
MBConv(x)=SE(DepthwiseConv(PointwiseConvup(x)))×ExpansionRatio+x
具体计算过程为:
- 扩展层(1×1卷积):将输入通道数扩展为原来的
t
t
t 倍(
t
t
t 为扩展系数)
x expanded = W expand ⋅ x x_{\text{expanded}} = W_{\text{expand}} \cdot x xexpanded=Wexpand⋅x - 深度可分离卷积(3×3或5×5):
x dw = DepthwiseConv ( x expanded ) x_{\text{dw}} = \text{DepthwiseConv}\left( x_{\text{expanded}} \right) xdw=DepthwiseConv(xexpanded) -
S
q
u
e
e
z
e
−
a
n
d
−
E
x
c
i
t
a
t
i
o
n
Squeeze-and-Excitation
Squeeze−and−Excitation 注意力机制:
x se = x dw ⋅ σ ( W 2 ⋅ ReLU ( W 1 ⋅ GAP ( x dw ) ) ) x_{\text{se}} = x_{\text{dw}} \cdot \sigma\left( W_{2} \cdot \text{ReLU}\left( W_{1} \cdot \text{GAP}\left( x_{\text{dw}} \right) \right) \right) xse=xdw⋅σ(W2⋅ReLU(W1⋅GAP(xdw))) - 投影层(1×1卷积):将通道数压缩回原始维度
x projected = W project ⋅ x se x_{\text{projected}} = W_{\text{project}} \cdot x_{\text{se}} xprojected=Wproject⋅xse - 残差连接(如果输入输出维度匹配):
Output = x projected + x \text{Output} = x_{\text{projected}} + x Output=xprojected+x
EfficientNetB6 的模型架构示意图如下:
🤓🤓🤓小周有话说:
把 M B C o n v MBConv MBConv 模块想象成一个高效的 信息处理工厂:
- 扩展层:就像把产品拆分成更小的零部件(扩展通道数),方便后续精细加工
- 深度可分离卷积:就像让不同的专家小组同时处理不同的零部件(深度卷积),大大提高了工作效率
- SE注意力机制:就像有个智能总监,时刻关注哪些零部件最重要(通道注意力),然后给重要的零部件分配更多资源
- 投影层:就像把处理好的零部件重新组装成最终产品(压缩回原始通道数)
- 残差连接:就像保留原材料的样本,确保最终产品不会在加工过程中失去原有优点
这种设计让模型既保持了强大的特征提取能力,又极大减少了计算量和参数数量。
3. 在项目中的应用
在动物分类项目中, E f f i c i e n t N e t − B 6 EfficientNet-B6 EfficientNet−B6 作为 特征提取器,其预训练的权重提供了强大的图像表示能力,采用了迁移学习策略,具体表现为:
-
冻结底层权重:保留预训练模型的前 200 200 200 层权重不变,仅训练顶层
L total = L CE + λ ⋅ L reg \mathcal{L}_{\text{total}} = \mathcal{L}_{\text{CE}} + \lambda \cdot \mathcal{L}_{\text{reg}} Ltotal=LCE+λ⋅Lreg
其中交叉熵损失 L CE = − ∑ i = 1 N y i log ( y ^ i ) \mathcal{L}{\text{CE}} = -\sum{i=1}^{N} y_i \log(\hat{y}i) LCE=−∑i=1Nyilog(y^i),正则化损失 L reg = ∣ W ∣ 2 2 \mathcal{L}{\text{reg}} = |\mathbf{W}|_2^2 Lreg=∣W∣22 -
自定义分类头:添加了包含批量归一化、 D r o p o u t Dropout Dropout 和 L 2 L2 L2 正则化的自定义层,以适应 100 100 100 类动物分类任务
Output = Softmax ( W ⋅ Dropout ( B N ( ReLU ( W h ⋅ x ) ) ) ) \text{Output} = \text{Softmax}(\mathbf{W} \cdot \text{Dropout}(\mathbf{BN}(\text{ReLU}(\mathbf{W}_h \cdot \mathbf{x})))) Output=Softmax(W⋅Dropout(BN(ReLU(Wh⋅x)))) -
混合精度训练:使用 f l o a t 16 float16 float16 进行计算,减少内存使用并加速训练过程,同时使用 f l o a t 32 float32 float32 进行 s o f t m a x softmax softmax 计算以保证数值稳定性
五、项目实现
1. 自定义学习率跟踪回调
- 创建一个自定义回调类
LRTracker
,继承自Callback
- 在初始化方法中创建一个空列表
lr_history
用于存储学习率历史 - 重写
on_epoch_end
方法,在每个训练周期结束时获取当前学习率并保存 - 同时将学习率添加到日志中,确保它被记录到训练历史中
# 自定义回调用于记录学习率
class LRTracker(Callback):
def __init__(self):
super(LRTracker, self).__init__()
self.lr_history = []
def on_epoch_end(self, epoch, logs=None):
current_lr = tf.keras.backend.get_value(self.model.optimizer.lr)
self.lr_history.append(current_lr)
logs = logs or {}
logs['lr'] = current_lr # 确保学习率被记录到history中
2. 参数配置
- 设置所有重要的超参数和路径
data_dir
指定数据集所在目录num_classes
设置为 100 100 100,表示有 100 100 100 种动物类别img_size
设置为 (456, 456) \textbf{(456, 456)} (456, 456),这是 EfficientNetB6 模型推荐的输入尺寸batch_size
设置为 12 12 12,平衡内存使用和训练效率epochs
设置为 20 20 20,限制训练时间- 其他参数包括早停耐心值、 L 2 L2 L2 正则化系数、 D r o p o u t Dropout Dropout 比率和初始学习率
# 参数配置
data_dir = '/kaggle/input/animals/Animal/Animal' # 数据集路径
num_classes = 100
img_size = (456, 456) # B6模型推荐尺寸
batch_size = 12 # 增加批次大小以提高GPU利用率
epochs = 20 # 减少轮数以适应12小时限制
patience = 5 # 早停等待轮数
l2_reg = 1e-4 # L2正则化系数
dropout_rate = 0.3 # Dropout比率
initial_lr = 1e-4 # 初始学习率
3. 数据增强和生成器设置
- 使用ImageDataGenerator创建数据增强管道,包括多种图像变换技术:
- 像素值归一化(
rescale
= 1. / 255 =1./255 =1./255) - 随机旋转(
rotation_range
= 30 =30 =30) - 水平和垂直平移(
width_shift_range
和height_shift_range
) - 剪切变换(
shear_range
) - 缩放变换(
zoom_range
) - 亮度调整(
brightness_range
) - 水平和垂直翻转
- 设置
20
%
20\%
20% 的数据作为验证集(
validation_split
= 0.2 =0.2 =0.2)
- 像素值归一化(
- 创建训练和验证数据生成器,从目录中加载图像
- 获取类别名称并打印训练和验证样本数量
# 创建数据增强
train_datagen = ImageDataGenerator(
rescale=1./255,
rotation_range=30,
width_shift_range=0.25,
height_shift_range=0.25,
shear_range=0.2,
zoom_range=0.3,
brightness_range=[0.7, 1.3],
horizontal_flip=True,
vertical_flip=True,
fill_mode='nearest',
validation_split=0.2 # 80%训练,20%验证
)
# 训练集生成器
train_generator = train_datagen.flow_from_directory(
data_dir,
target_size=img_size,
batch_size=batch_size,
class_mode='categorical',
subset='training',
shuffle=True,
seed=42
)
# 验证集生成器
val_generator = train_datagen.flow_from_directory(
data_dir,
target_size=img_size,
batch_size=batch_size,
class_mode='categorical',
subset='validation',
shuffle=False, # 验证集不洗牌,确保一致性
seed=42
)
终端划分完毕的数据集信息:
Found 31803 images belonging to 100 classes.
Found 7901 images belonging to 100 classes.
训练样本数: 31803, 验证样本数: 7901
4. 学习率调度函数
- 定义一个自定义学习率调度函数
accelerated_lr_schedule
- 使用 预热策略:前 3 3 3 个周期逐步增加学习率到初始值
- 第 4 4 4 到第 10 10 10 个周期保持初始学习率
- 第 10 10 10 个周期后开始 指数衰减学习率
- 这种策略有助于模型快速收敛并 防止过拟合
# 加速学习率调度
def accelerated_lr_schedule(epoch):
"""更激进的学习率调度以快速收敛"""
warmup_epochs = 3
decay_start = 10
if epoch < warmup_epochs:
# 学习率预热
return initial_lr * (epoch + 1) / warmup_epochs
elif epoch < decay_start:
# 保持高学习率
return initial_lr
else:
# 指数衰减
return initial_lr * math.exp(0.1 * (decay_start - epoch))
5. 模型构建函数
- 使用 EfficientNetB6 作为基础模型,加载在 I m a g e N e t ImageNet ImageNet 上预训练的权重
- 冻结前 200 200 200 层,只训练后面的层(迁移学习 策略)
- 添加自定义顶层:
- 全局平均池化层
- 全连接层( 1024 1024 1024 个神经元, R e L U ReLU ReLU 激活, L 2 L2 L2 正则化)
- 批归一化层
- D r o p o u t Dropout Dropout 层(防止过拟合)
- 另一个全连接层( 512 512 512 个神经元)
- 另一个批归一化和 D r o p o u t Dropout Dropout 层
- 使用 L a m b d a Lambda Lambda 层将数据类型转换为 f l o a t 32 float32 float32,确保与混合精度训练兼容
- 最后添加输出层( 100 100 100 个神经元, s o f t m a x softmax softmax 激活)
- 编译模型,使用 A d a m Adam Adam 优化器和分类交叉熵损失函数
# 构建模型
def create_model():
base_model = EfficientNetB6(
include_top=False,
weights='imagenet',
input_shape=(img_size[0], img_size[1], 3),
pooling=None
)
# 冻结底层,微调上层
base_model.trainable = True
for layer in base_model.layers[:200]:
layer.trainable = False
# 添加自定义顶层
x = base_model.output
x = GlobalAveragePooling2D()(x)
x = Dense(1024, activation='relu', kernel_regularizer=l2(l2_reg))(x)
x = BatchNormalization()(x)
x = Dropout(dropout_rate)(x)
x = Dense(512, activation='relu', kernel_regularizer=l2(l2_reg))(x)
x = BatchNormalization()(x)
x = Dropout(dropout_rate/2)(x)
# 修复:使用Lambda层进行类型转换
x = Lambda(lambda t: tf.cast(t, tf.float32))(x)
predictions = Dense(num_classes, activation='softmax', dtype=tf.float32)(x)
model = Model(inputs=base_model.input, outputs=predictions)
model.compile(
optimizer=Adam(learning_rate=initial_lr),
loss='categorical_crossentropy',
metrics=['accuracy']
)
return model
6. 回调函数设置
- 创建多个回调函数以增强训练过程:
EarlyStopping
: 当验证损失不再改善时停止训练,防止过拟合ReduceLROnPlateau
: 当验证损失停滞时降低学习率ModelCheckpoint
: 保存验证损失最低的模型LearningRateScheduler
: 使用自定义学习率调度函数LRTracker
: 自定义学习率跟踪回调
# 创建学习率跟踪器
lr_tracker = LRTracker()
# 回调函数
callbacks = [
EarlyStopping(
monitor='val_loss',
patience=patience,
verbose=1,
restore_best_weights=True,
min_delta=0.001
),
ReduceLROnPlateau(
monitor='val_loss',
factor=0.5,
patience=2,
min_lr=1e-6,
verbose=1
),
ModelCheckpoint(
'/kaggle/working/best_model.keras',
monitor='val_loss',
save_best_only=True,
verbose=1
),
LearningRateScheduler(accelerated_lr_schedule, verbose=1),
lr_tracker # 添加学习率跟踪器
]
7. 开始训练!
- 使用
model.fit()
方法训练模型 - 指定训练和验证数据生成器
- 计算每个周期的步骤数(样本数除以批次大小)
- 训练完成后,确保学习率历史被正确记录
- 将训练历史保存到 Excel 文件中,便于后续分析
# 训练模型
print("开始训练模型...")
history = model.fit(
train_generator,
steps_per_epoch=train_generator.samples // batch_size,
validation_data=val_generator,
validation_steps=val_generator.samples // batch_size,
epochs=epochs,
callbacks=callbacks,
verbose=1
)
# 确保学习率被记录到历史中
if 'lr' not in history.history:
history.history['lr'] = lr_tracker.lr_history
# 保存训练历史
history_df = pd.DataFrame(history.history)
history_df.to_excel('/kaggle/working/training_history.xlsx', index=False)
训练输出示例如下:
开始训练模型…
Epoch 1: LearningRateScheduler setting learning rate to 3.3333333333333335e-05.
Epoch 1/25
2650/2650 ━━━━━━━━━━━━━━━━━━━━ 0s 845ms/step - accuracy: 0.0342 - loss: 5.2547
Epoch 1: val_loss improved from inf to 3.71864, saving model to /kaggle/working/best_model.keras
2650/2650 ━━━━━━━━━━━━━━━━━━━━ 2982s 1s/step - accuracy: 0.0342 - loss: 5.2546 - val_accuracy: 0.2088 - val_loss: 3.7186 - learning_rate: 3.3333e-05 - lr: 3.3333e-05
Epoch 2: LearningRateScheduler setting learning rate to 6.666666666666667e-05.
…
Epoch 25: val_loss improved from 0.68474 to 0.65006, saving model to /kaggle/working/best_model.keras
2650/2650 ━━━━━━━━━━━━━━━━━━━━ 2277s 859ms/step - accuracy: 0.9122 - loss: 0.5337 - val_accuracy: 0.9007 - val_loss: 0.6501 - learning_rate: 2.4660e-05 - lr: 2.4660e-05
Restoring model weights from the end of the best epoch: 25.
在验证集上评估模型…
659/659 ━━━━━━━━━━━━━━━━━━━━ 430s 613ms/step - accuracy: 0.8823 - loss: 0.7223
验证集准确率: 0.8946
生成预测结果…
659/659 ━━━━━━━━━━━━━━━━━━━━ 420s 612ms/step
随机抽取验证样本可视化…
==================================================
所有操作已完成!
最佳模型已保存为: best_model.keras
训练历史已保存为: training_history.xlsx
类别准确率报告已保存为: class_accuracy_report.xlsx
验证集准确率: 0.8946
总训练时间: 9小时 49分钟 18秒
==================================================
ps
:模型虽有设置早停机制,最优模型的保存仍是设置的上限(epoch
=
25
=25
=25),受限于 kaggle 的 GPU 单次使用时间上限,小周没有进一步训练,有条件的 UU 可以设置更大的训练轮数和早停耐心值
8. 模型评估和预测
- 在验证集上评估模型性能,计算 损失 和 准确率
- 生成预测结果,获取真实标签和预测标签
- 计算 混淆矩阵,显示模型在 每个类别 上的预测情况
- 使用分类报告获取每个类别的精确率、召回率和 F 1 F1 F1 分数
- 分析每个类别的主要错误类型和错误比例
- 将所有类别级别的指标保存到 Excel 文件中
# 加载最佳模型
model = load_model('/kaggle/working/best_model.keras')
# 在验证集上评估
print("\n在验证集上评估模型...")
val_loss, val_acc = model.evaluate(val_generator)
print(f"验证集准确率: {val_acc:.4f}")
# 生成预测结果
print("\n生成预测结果...")
y_true = val_generator.classes
y_pred = model.predict(val_generator, verbose=1).argmax(axis=1)
# 计算混淆矩阵
cm = confusion_matrix(y_true, y_pred)
# 分析每个类别的准确率和错误情况
class_report = classification_report(y_true, y_pred, target_names=class_names, output_dict=True)
class_metrics = []
for i, class_name in enumerate(class_names):
# 类别准确率
precision = class_report[class_name]['precision']
recall = class_report[class_name]['recall']
f1 = class_report[class_name]['f1-score']
support = class_report[class_name]['support']
# 主要错误类别
class_errors = cm[i].copy()
class_errors[i] = 0 # 忽略正确预测
if np.sum(class_errors) > 0:
main_error_idx = np.argmax(class_errors)
main_error_class = class_names[main_error_idx]
error_count = class_errors[main_error_idx]
error_percent = error_count / np.sum(class_errors) * 100
else:
main_error_class = "无错误"
error_count = 0
error_percent = 0
class_metrics.append({
'类别': class_name,
'精确率': precision,
'召回率': recall,
'F1分数': f1,
'样本数': support,
'主要误判类别': main_error_class,
'误判数量': error_count,
'误判占比(%)': error_percent
})
# 保存类别准确率结果
metrics_df = pd.DataFrame(class_metrics)
metrics_df.to_excel('/kaggle/working/class_accuracy_report.xlsx', index=False)
9. 样本预测可视化
- 随机选择 12 12 12 个验证样本进行可视化
- 对每个样本进行预测,获取预测标签和置信度
- 创建3×4的子图布局,显示每个样本图像、真实标签、预测标签和置信度
- 正确预测用绿色标题,错误预测用红色标题
# 随机抽取12个验证样本可视化(节省空间)
print("\n随机抽取验证样本可视化...")
sample_indices = random.sample(range(len(val_generator.filepaths)), 12)
sample_images = []
sample_true_labels = []
sample_pred_labels = []
sample_probs = []
for idx in sample_indices:
img_path = val_generator.filepaths[idx]
img = tf.keras.preprocessing.image.load_img(img_path, target_size=img_size)
img_array = tf.keras.preprocessing.image.img_to_array(img) / 255.0
sample_images.append(img_array)
sample_true_labels.append(class_names[y_true[idx]])
# 获取预测概率
pred_probs = model.predict(np.expand_dims(img_array, axis=0), verbose=0)[0]
pred_idx = np.argmax(pred_probs)
sample_pred_labels.append(class_names[pred_idx])
sample_probs.append(pred_probs[pred_idx])
# 可视化结果
plt.figure(figsize=(15, 12))
for i in range(12):
plt.subplot(3, 4, i+1)
plt.imshow(sample_images[i])
color = 'green' if sample_true_labels[i] == sample_pred_labels[i] else 'red'
title = f"True: {sample_true_labels[i]}\nPred: {sample_pred_labels[i]}\nProb: {sample_probs[i]:.2f}"
plt.title(title, fontsize=10, color=color)
plt.axis('off')
plt.tight_layout()
plt.savefig('/kaggle/working/sample_predictions.png', dpi=150)
plt.close()
六、结果展示
训练过程中记录的关键指标包括:
- 损失与准确率曲线
- 训练损失与验证损失的变化趋势
- 训练准确率与验证准确率的提升过程
2. 混淆矩阵
展示模型在验证集上的分类结果,对角线元素表示正确分类的样本数,非对角线元素表示分类错误的样本数
3. 类别性能分析
每个动物类别的精确率、召回率和 F1 分数,帮助识别模型表现较好和较差的类别
模型在验证集抽取的样本预测展示如下所示:
Reference
如果你喜欢我的文章,不妨给小周一个免费的点赞和关注吧!