代码如下:# 导入数值计算库numpy,用于数组操作
import numpy as np
# 导入深度学习框架tensorflow
import tensorflow as tf
# 从tensorflow.keras.datasets中导入MNIST手写数字数据集
from tensorflow.keras.datasets import mnist
# 从tensorflow.keras.models中导入Sequential序列模型(用于堆叠神经网络层)
from tensorflow.keras.models import Sequential
# 从tensorflow.keras.layers中导入卷积层、池化层、展平层、全连接层、dropout层
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout
# 从tensorflow.keras.regularizers中导入L2正则化(防止过拟合)
from tensorflow.keras.regularizers import l2
# 从tensorflow.keras.callbacks中导入早停回调(防止过拟合,提前终止训练)
from tensorflow.keras.callbacks import EarlyStopping
# 从tensorflow.keras.preprocessing.image中导入图像数据生成器(用于数据增强)
from tensorflow.keras.preprocessing.image import ImageDataGenerator
# 从sklearn.model_selection中导入train_test_split(用于手动拆分训练集和验证集)
from sklearn.model_selection import train_test_split # 新增:手动拆分验证集
# 导入matplotlib.pyplot,用于数据可视化
import matplotlib.pyplot as plt
# 修复中文显示问题(设置中文字体为黑体)
plt.rcParams['font.sans-serif'] = ['SimHei']
# 修复负号显示异常问题
plt.rcParams['axes.unicode_minus'] = False
# 1. 定义数据加载、预处理与去重复函数
def load_and_preprocess_data():
# 加载MNIST数据集,返回训练集(x_train图像数据、y_train标签)和测试集(x_test图像数据、y_test标签)
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# 将训练集图像从28x28二维数组展平为一维数组(便于检测重复样本)
x_train_flat = x_train.reshape(len(x_train), -1)
# 提取训练集唯一样本的索引(axis=0按行去重,return_index返回唯一样本的原始索引)
_, train_unique_idx = np.unique(x_train_flat, axis=0, return_index=True)
# 打印训练集重复样本数量,并提示已删除
print(f"训练集重复样本数:{len(x_train) - len(train_unique_idx)},已删除")
# 根据唯一索引筛选训练集,删除重复样本
x_train, y_train = x_train[train_unique_idx], y_train[train_unique_idx]
# 将测试集图像从28x28二维数组展平为一维数组
x_test_flat = x_test.reshape(len(x_test), -1)
# 提取测试集唯一样本的索引
_, test_unique_idx = np.unique(x_test_flat, axis=0, return_index=True)
# 打印测试集重复样本数量,并提示已删除
print(f"测试集重复样本数:{len(x_test) - len(test_unique_idx)},已删除")
# 根据唯一索引筛选测试集,删除重复样本
x_test, y_test = x_test[test_unique_idx], y_test[test_unique_idx]
# 预处理:添加通道维度(适配CNN输入格式,从(28,28)转为(28,28,1)),并归一化到[0,1]区间
x_train = np.expand_dims(x_train, axis=-1) / 255.0
# 测试集执行相同的预处理操作
x_test = np.expand_dims(x_test, axis=-1) / 255.0
# 手动拆分训练集为训练子集和验证集(替代model.fit中的validation_split参数)
x_train_sub, x_val, y_train_sub, y_val = train_test_split(
x_train, y_train, test_size=0.1, random_state=42 # test_size=0.1表示10%作为验证集,random_state固定随机种子保证可复现
)
# 返回处理后的训练子集、训练子集标签、验证集、验证集标签、测试集、测试集标签
return x_train_sub, y_train_sub, x_val, y_val, x_test, y_test
# 2. 定义优化后的CNN模型构建函数
def build_optimized_cnn_model():
# 初始化序列模型(按顺序堆叠网络层)
model = Sequential([
# 卷积层:64个3x3卷积核,ReLU激活函数,输入形状为(28,28,1),添加L2正则化(系数1e-4)
Conv2D(64, (3, 3), activation='relu', input_shape=(28, 28, 1), kernel_regularizer=l2(1e-4)),
# 最大池化层:2x2池化窗口(降低特征图尺寸,保留关键特征)
MaxPooling2D((2, 2)),
# 卷积层:再次使用64个3x3卷积核,ReLU激活函数,添加L2正则化
Conv2D(64, (3, 3), activation='relu', kernel_regularizer=l2(1e-4)),
# 最大池化层:2x2池化窗口
MaxPooling2D((2, 2)),
# 展平层:将二维特征图转为一维向量(适配全连接层输入)
Flatten(),
# Dropout层:随机失活50%的神经元(防止过拟合)
Dropout(0.5),
# 全连接层:64个神经元,ReLU激活函数,添加L2正则化
Dense(64, activation='relu', kernel_regularizer=l2(1e-4)),
# 输出层:10个神经元(对应0-9共10个数字),softmax激活函数(输出概率分布)
Dense(10, activation='softmax')
])
# 编译模型:设置优化器、损失函数和评估指标
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4), # Adam优化器,学习率1e-4
loss='sparse_categorical_crossentropy', # 稀疏分类交叉熵(适用于整数标签,无需独热编码)
metrics=['accuracy'] # 评估指标:准确率
)
# 返回构建并编译好的模型
return model
# 3. 核心流程(程序入口)
if __name__ == "__main__":
# 调用数据加载与预处理函数,获取处理后的各数据集
x_train_sub, y_train_sub, x_val, y_val, x_test, y_test = load_and_preprocess_data()
# 初始化图像数据生成器(用于数据增强,提升模型泛化能力)
datagen = ImageDataGenerator(
rotation_range=10, # 随机旋转角度范围(-10°到10°)
width_shift_range=0.1, # 随机水平偏移(图像宽度的10%)
height_shift_range=0.1, # 随机垂直偏移(图像高度的10%)
zoom_range=0.1 # 随机缩放范围(0.9到1.1倍)
)
# 让数据生成器适配训练子集(学习训练集的统计特征)
datagen.fit(x_train_sub) # 适配训练子集
# 调用模型构建函数,创建优化后的CNN模型
model = build_optimized_cnn_model()
# 初始化早停回调:监控验证集损失,连续3轮无下降则停止训练,并恢复最优权重
early_stopping = EarlyStopping(
monitor='val_loss', # 监控指标:验证集损失
patience=3, # 耐心值:3轮无改善则停止
restore_best_weights=True # 恢复训练过程中验证损失最优的模型权重
)
# 训练模型:使用数据增强后的训练子集,验证集使用手动拆分的x_val/y_val
history = model.fit(
datagen.flow(x_train_sub, y_train_sub, batch_size=32), # 生成增强后的训练数据,批次大小32
epochs=20, # 最大训练轮次20
validation_data=(x_val, y_val), # 验证集:手动拆分的x_val和y_val(替代validation_split)
callbacks=[early_stopping], # 启用早停回调
verbose=1 # 训练过程可视化:显示每轮训练的损失和准确率
)
# 在测试集上评估模型性能(verbose=0表示不显示评估过程)
test_loss, test_acc = model.evaluate(x_test, y_test, verbose=0)
# 打印测试集准确率(保留4位小数)
print(f"\n测试集准确率: {test_acc:.4f}")
# 可视化训练损失与验证损失、训练准确率与验证准确率曲线
plt.figure(figsize=(12, 4)) # 设置图像大小:宽12英寸,高4英寸
plt.subplot(1, 2, 1) # 创建1行2列的子图,当前绘制第1个子图(损失曲线)
plt.plot(history.history['loss'], label='训练损失', color='blue') # 绘制训练损失曲线
plt.plot(history.history['val_loss'], label='验证损失', color='orange') # 绘制验证损失曲线
plt.title('训练 vs 验证损失') # 设置子图标题
plt.xlabel('训练轮次(Epochs)') # x轴标签
plt.ylabel('损失值') # y轴标签
plt.legend() # 显示图例
plt.subplot(1, 2, 2) # 当前绘制第2个子图(准确率曲线)
plt.plot(history.history['accuracy'], label='训练准确率', color='blue') # 绘制训练准确率曲线
plt.plot(history.history['val_accuracy'], label='验证准确率', color='orange') # 绘制验证准确率曲线
plt.title('训练 vs 验证准确率') # 设置子图标题
plt.xlabel('训练轮次(Epochs)') # x轴标签
plt.ylabel('准确率') # y轴标签
plt.legend() # 显示图例
plt.tight_layout() # 自动调整子图间距,避免重叠
plt.show() # 显示图像
# 可视化随机测试样本的真实标签与预测标签
num_samples = 5 # 设定要可视化的样本数量
# 从测试集中随机选择5个样本的索引(replace=False表示不重复选择)
random_indices = np.random.choice(len(x_test), num_samples, replace=False)
plt.figure(figsize=(15, 3)) # 设置图像大小:宽15英寸,高3英寸
for i, idx in enumerate(random_indices): # 遍历每个随机索引
plt.subplot(1, num_samples, i + 1) # 创建1行5列的子图,绘制当前样本
plt.imshow(x_test[idx].reshape(28, 28), cmap='gray') # 显示测试图像(转为28x28,灰度模式)
# 预测当前样本的标签(argmax获取概率最大的类别索引)
pred_label = np.argmax(model.predict(x_test[idx:idx + 1], verbose=0)[0])
# 设置子图标题:显示真实标签和预测标签,字体大小10
plt.title(f"真实: {y_test[idx]}\n预测: {pred_label}", fontsize=10)
plt.axis('off') # 隐藏坐标轴
plt.tight_layout() # 自动调整子图间距
plt.show() # 显示图像