简介:面向机器学习初学者和需要完成图像分类实战作业的学生,这份压缩包提供了基于Fashion-MNIST数据集的神经网络分类完整代码与配套讲解。Fashion-MNIST包含T恤、连衣裙、运动鞋等10类28×28灰度衣物图像,任务是训练模型自动判断图像类别。代码依次完成图像归一化与标签one-hot编码,构建由卷积层、池化层与全连接层组成的神经网络,配置交叉熵损失和Adam优化器进行前向传播与反向传播训练,并在测试集上评估准确率。压缩包共2个文件,含可直接运行的Python源码和逐步解析原理的Word文档,包体仅559KB,轻量易用。目前已有297人学习,借助该代码可完整走通图像分类流程,同时通过文档理解数据预处理、网络结构设计与调参思路,为课程设计或入门实践提供可靠范本。
1. 神经网络跑 Fashion 分类:一份 homework6 代码包能带你走多远
手头这份神经网络实现fashion数据分类代码.zip,解压后就是homework6目录,里面一个main.py加一份 doc 说明文档,典型的学生作业结构。但别因为它挂着 homework 的名头就轻视——Fashion-MNIST 是 Zalando 做的 MNIST 替代品,10 类衣物灰度图,28×28 分辨率,6000 张训练图配 1000 张测试图每类,任务是把 T 恤、连衣裙、运动鞋这些分对。这个数据集放在今天依然是入门神经网络最合适的练手对象:它比手写数字难一点,又不至于一上来就上 ImageNet 这种量级。这份资源能帮你解决的实际问题是——把「神经网络分类」从概念落到可运行的完整流程:数据预处理、模型搭建、训练、评估、调参一步步走完,而且代码是作业级规模,适合照着改而不是当黑匣子跑完就完。适合两类人:刚学完深度学习理论、需要一份能跑通的参考实现的学生;以及想快速验证某个网络改动在 Fashion-MNIST 上效果如何的工程人员。
2. 先搞懂 Fashion-MNIST:数据长什么样,预处理到底在做什么
2.1 数据集的真实结构:28×28 的灰度图藏了什么信息
Fashion-MNIST 每一张图是 28×28 像素的灰度图像,像素值范围 0 到 255,0 是纯黑,255 是纯白。10 个类别分别是 T 恤、裤子、套头衫、连衣裙、外套、凉鞋、衬衫、运动鞋、包、踝靴。这里有个容易忽略的点:类别是从 0 到 9 的整数标签,不是字符串。数据集本身类别均衡,每类训练图 6000 张、测试图 1000 张,所以直接用准确率做评估指标是合理的——这跟处理不平衡数据集时得看 F1 或 AUC 是两码事。
加载方式最省事的是 keras 自带的数据集接口,不用手动下载和解析像素文件。常见的做法是直接用tensorflow.keras.datasets.fashion_mnist.load_data(),第一次运行会自动下载到~/.keras/datasets目录,之后都是从本地缓存读取。
import numpy as np import tensorflow as tf from tensorflow.keras.datasets import fashion_mnist # 加载数据集,返回的是 numpy 数组 (x_train, y_train), (x_test, y_test) = fashion_mnist.load_data() print(f"训练集 shape: {x_train.shape}, 标签 shape: {y_train.shape}") print(f"测试集 shape: {x_test.shape}, 像素值范围: {x_train.min()} ~ {x_train.max()}")这段代码里load_data()返回四个 numpy 数组,x_train的 shape 是 (60000, 28, 28),表示 6 万张 28×28 的灰度图,标签y_train是一维数组,每个元素是 0 到 9 的整数。打印像素值范围这一步很重要——如果最小值和最大值不是 0 到 255,说明数据源有问题,后面归一化的写法就得跟着调整。
2.2 归一化的两个流派:除 255 还是标准化
归一化是这份作业里第一个容易翻车的地方。Fashion-MNIST 的像素是 0~255 的整数,直接喂给神经网络,数值范围太大,会让梯度更新不稳定。最常见的做法是直接除以 255,把像素缩放到 0~1 之间。另一流派是算均值和标准差做标准化,但图像数据上 0~1 归一化已经够用,标准化更多用在特征尺度差异大的表格数据上。
关键细节:归一化要在训练和测试集上做同样的操作,而且要保证数据类型从 uint8 转成 float32,否则除以 255 后小数部分直接丢光。同时因为后面要用卷积层,需要把单通道的 (28, 28) 变成 (28, 28, 1),reshape这一步很多新手会漏。
# 像素值从 uint8 转 float32 再归一化,这一步漏了就等着模型不收敛吧 x_train = x_train.astype('float32') / 255.0 x_test = x_test.astype('float32') / 255.0 # 给灰度图补上通道维度,Conv2D 要求输入是 4D 张量 (batch, height, width, channels) x_train = x_train.reshape(-1, 28, 28, 1) x_test = x_test.reshape(-1, 28, 28, 1) # one-hot 编码:把标签从整数变成 10 维向量 y_train_onehot = tf.keras.utils.to_categorical(y_train, num_classes=10) y_test_onehot = tf.keras.utils.to_categorical(y_test, num_classes=10) print(f"归一化后像素范围: {x_train.min():.2f} ~ {x_train.max():.2f}") print(f"one-hot 标签示例: {y_train_onehot[0]}")逻辑说明:astype('float32')必须在除法之前做,numpy 里整数数组做除法会得到浮点结果,但为了避免隐式转换的坑,显式转换更稳妥。reshape(-1, 28, 28, 1)里的-1表示自动推断该维度大小,实际是训练样本数 60000。to_categorical 把整数标签转成 one-hot 向量,比如标签 3 变成[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]。
这里的参数选择有讲究:num_classes=10必须和输出层神经元数量一致,写少了训练时会直接报维度不匹配错误。如果不想用 one-hot,也可以在模型里用sparse_categorical_crossentropy损失函数配整数标签,两条路都能走通,但混着用就会出幺蛾子——这个坑后面专门讲。
2.3 数据预处理的意义:为什么非要折腾这一步
很多初学者觉得预处理是浪费时间,直接原图扔进网络不就行了?实际上神经网络的训练过程对输入数据的分布极其敏感。像素值 0~255 和 0~1 的差异,直接决定了权重初始化的有效性。神经网络权重通常按小随机数初始化,如果输入值过大,前向传播时加权和会很大,激活函数(比如 sigmoid)直接饱和,梯度趋近于零,反向传播时参数几乎不更新,训练就卡死了。归一化本质上是让数据分布落在激活函数的敏感区间内,这是模型能学进去的前提。
one-hot 编码的作用也类似:分类任务常用 softmax 做输出层,softmax 的输出是一个概率分布,交叉熵损失函数计算的是预测分布和真实分布的差异。如果标签是整数 3,模型输出是 10 维向量,两者没法直接做损失计算,必须转成同维度的表示。当然,sparse_categorical_crossentropy可以内部帮你做这个转换,但从理解角度,先手写 one-hot 再喂给模型,能更清楚数据在每一层流动时的形状变化。
3. 模型搭建:从 MLP 到卷积网络,参数量差了一个量级
3.1 为什么作业里要有卷积层:先看看全连接层的参数量爆炸
如果只用全连接层处理 28×28 的图像,输入层就是 784 个神经元。假设隐藏层设 128 个神经元,这一层的参数量是 784×128 + 128(偏置),大约 10 万。再叠几层,参数量轻松上百万。而 Fashion-MNIST 只有 6 万张训练图,百万级参数的网络很容易过拟合——记住训练集容易,测试集上泛化差。更要命的是全连接层把每个像素当成独立特征,完全忽略了像素在空间上的相邻关系。一张图里,某个像素和它上下左右的像素构成的是纹理、边缘、形状这些局部特征,全连接层学不到这种空间结构。
卷积层的核心思想是局部连接和权重共享。一个 3×3 的卷积核在图像上滑动,每次只看 3×3 的区域,提取局部特征;不同位置共享同一组权重,参数量大幅下降。池化层(MaxPooling2D)做降采样,保留主要特征的同时减少计算量,也提供一定的平移不变性。这就是作业描述里「隐藏层通常包含多个卷积层和池化层」的技术原因。
3.2 搭建一个能跑到 90% 以上的 CNN:结构、参数、激活函数选择
给出一个在 Fashion-MNIST 上验证过多次的经典结构:两个卷积块 + 全连接分类头。这个结构参数量适中,训练速度快,准确率能到 92% 左右,很适合作业场景。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, Input def build_cnn(): model = Sequential([ # 第一个卷积块:32 个 3x3 卷积核,padding='same' 保持空间尺寸 Input(shape=(28, 28, 1)), Conv2D(32, (3, 3), activation='relu', padding='same'), MaxPooling2D((2, 2)), # 第二个卷积块:64 个 3x3 卷积核,特征通道翻倍 Conv2D(64, (3, 3), activation='relu', padding='same'), MaxPooling2D((2, 2)), # 展平后接全连接层 Flatten(), Dense(128, activation='relu'), Dropout(0.3), # 输出层:10 个神经元,softmax 输出概率分布 Dense(10, activation='softmax') ]) return model model = build_cnn() model.summary()逻辑说明:Input(shape=(28, 28, 1))显式声明输入形状,必须和预处理后的数据 shape 一致;Conv2D(32, (3, 3), activation='relu', padding='same')表示 32 个 3×3 卷积核,padding='same'让卷积后特征图尺寸保持不变,valid则会缩小;MaxPooling2D((2, 2))把特征图宽高各减半,28×28 经过两次池化变成 7×7;Dropout(0.3)是正则化手段,训练时随机丢弃 30% 的神经元,防止过拟合;输出层Dense(10, activation='softmax')输出 10 类概率。
参数调整的优先级:先改卷积核数量(32→64→128),再调 Dropout(0.3→0.5),最后才动网络层数。卷积核数量决定特征提取能力,欠拟合就加,过拟合就减。Dropout 只在过拟合明显时加大,但太大(超过 0.6)会让模型难以收敛。
3.3 对比才是学习的捷径:跑一个 MLP 做基线
只给 CNN 不给对比,就不知道 CNN 到底好在哪。这里用一个两层全连接网络做基线,和上面的 CNN 对比参数量和准确率。这个对比的价值在于让你直观感受「模型容量」和「结构先验」对结果的影响。
def build_mlp(): model = Sequential([ Input(shape=(28, 28, 1)), Flatten(), # 28x28x1 = 784,展平成向量 Dense(128, activation='relu'), Dense(10, activation='softmax') ]) return model mlp = build_mlp() mlp.summary()对比两份model.summary()输出就能看到:MLP 的参数量在 10 万级别,CNN 因为有卷积核权重共享,两层卷积的参数量反而更小,但表达能力和泛化能力远超 MLP。这就是「卷积网络天生适合图像」这句话的实际含义。做作业的时候,建议两个模型都跑一遍,把准确率记录成表格,写报告的时候素材就齐了。
4. 训练闭环:损失函数、优化器、回调函数和调参顺序
4.1 交叉熵配 Adam:分类任务最稳的组合,但学习率不是默认就好
模型搭完就得选损失函数和优化器。多分类任务标准配置是交叉熵损失 + Adam 优化器。交叉熵衡量预测概率分布和真实分布的差异,配合 softmax 输出层,数学上配合得最自然。Adam 是自适应学习率优化器,能根据梯度的历史信息自动调整每个参数的学习率,不用手动设置学习率衰减策略,是工程上的默认选择。
但这有个常见误区:Adam()默认学习率是 0.001,这个值在很多任务上好用,但并不是所有场景都最优。Fashion-MNIST 上 0.001 可以,但如果发现 loss 下降速度异常慢或者直接发散,第一件事就是把学习率调低到 0.0001 看看。优化器参数不是不能动,很多人把优化器当黑匣子默认到底,等到不收敛才开始怀疑数据集,实际上学习率是最容易排查的变量。
from tensorflow.keras.optimizers import Adam from tensorflow.keras.losses import CategoricalCrossentropy from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping model.compile( optimizer=Adam(learning_rate=0.001), loss=CategoricalCrossentropy(), metrics=['accuracy'] ) # 自动保存验证集准确率最高的权重,防止跑完没存模型 checkpoint = ModelCheckpoint( 'best_model.h5', monitor='val_accuracy', mode='max', save_best_only=True, verbose=1 ) # 验证集 loss 连续 5 轮不下降就停止,省时间也防过拟合 early_stop = EarlyStopping( monitor='val_loss', patience=5, restore_best_weights=True ) history = model.fit( x_train, y_train_onehot, batch_size=64, epochs=30, validation_split=0.2, # 从训练集里切 20% 做验证集 callbacks=[checkpoint, early_stop], verbose=1 )逻辑说明:compile阶段把优化器、损失函数、评估指标绑定到模型上;ModelCheckpoint在每轮 epoch 结束后对比验证集准确率,只保存最好的权重文件,这样即使训练中断也有后悔药;EarlyStopping监测验证集 loss,连续 5 轮不下降就提前终止训练,restore_best_weights=True会把权重回滚到验证集最优的那一轮;validation_split=0.2是直接从训练集尾部切 20% 做验证集,训练集实际只用 48000 张,验证集 12000 张。
这里有个容易被忽略的点:验证集和测试集是两码事。验证集用来调超参数和决定模型保存时机,测试集只在最终评估时用一次。如果用测试集来调参,你对测试集的拟合就会变成一种隐式的过拟合,最终报告的准确率是虚高的。
4.2 训练历史曲线:判断模型状态最快的方式是画图,不是看数字
model.fit()返回的history对象里记录了每轮的 loss 和 accuracy。把训练曲线画出来,能一眼看出欠拟合还是过拟合。这个可视化步骤在作业报告里也几乎是必考的加分项。
import matplotlib.pyplot as plt def plot_training_history(history): fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) # 左边画 loss 曲线 ax1.plot(history.history['loss'], label='train_loss') ax1.plot(history.history['val_loss'], label='val_loss') ax1.set_xlabel('Epoch') ax1.set_ylabel('Loss') ax1.legend() # 右边画准确率曲线 ax2.plot(history.history['accuracy'], label='train_acc') ax2.plot(history.history['val_accuracy'], label='val_acc') ax2.set_xlabel('Epoch') ax2.set_ylabel('Accuracy') ax2.legend() plt.tight_layout() plt.show() plot_training_history(history)判读标准:训练 loss 下降但验证 loss 先降后升,是典型的过拟合信号,这时候加大 Dropout 或增加数据增强;训练 loss 和验证 loss 都居高不下,是欠拟合,需要增加模型容量或训练轮数;验证 loss 出现锯齿状抖动,大概率是学习率偏大,降到 0.0001 再试。
4.3 调参顺序先放这:新手按这个顺序来,别乱动结构
调参最忌讳一上来就改网络结构。推荐顺序是:先固定网络结构和优化器,把学习率调到一个能稳定收敛的值;然后调 batch size(16、32、64、128),batch size 影响梯度估计的稳定性;再调 Dropout 比例控制过拟合;最后才考虑增加或减少卷积层。每个变量单独调,不要一次改两个以上参数,否则模型表现变了你根本不知道是哪个因素的功劳。
Fashion-MNIST 上我的经验值:batch size 64、epochs 30、early stopping patience 5、Dropout 0.3,这个配置下 CNN 能在 15 轮左右达到 91%+ 的验证准确率。如果发现某个超参数改动后准确率提升超过 2%,那才是真正有效的改动,0.5% 以内的波动基本上只是随机种子不同造成的噪声而已。
5. 避坑手册:这份作业代码里最容易翻车的五个隐蔽坑
5.1 训练集准确率 99%,测试集只有 72%:标准化到底做没做
这是最典型的现象级翻车:训练曲线完美,val_accuracy 也看着正常,一跑测试集就崩。原因一般是测试集数据忘记做归一化,或者归一化方式跟训练集不一致。像素值 0~255 直接喂进网络和 0~1 的输入分布完全不同,权重是为归一化后的数据学出来的,测试时不归一化相当于给模型喂了「另一种格式」的数据。解决方法是把预处理逻辑封装成函数,训练集和测试集走同一套流程:
def preprocess(images, labels): images = images.astype('float32') / 255.0 images = images.reshape(-1, 28, 28, 1) labels_onehot = tf.keras.utils.to_categorical(labels, num_classes=10) return images, labels_onehot # 训练集和测试集走同一个函数,别手写两遍 x_train_processed, y_train_processed = preprocess(x_train_raw, y_train_raw) x_test_processed, y_test_processed = preprocess(x_test_raw, y_test_raw)从那以后我每次写预处理都会把训练测试两套数据塞进同一个函数处理,杜绝手写两份逻辑时漏掉其中一份的隐患。
5.2 报错ValueError: Shapes (None, 10) and (None, 1) are incompatible:标签编码和损失函数不匹配
现象是model.fit直接报维度不匹配错误。原因是把整数标签传给模型的同时用了categorical_crossentropy损失函数,而它期望的是 one-hot 向量。模型输出是 10 个神经元,输出 shape 是 (batch, 10),标签却是 (batch, 1),对不上。解决方法是二选一:要么标签做 one-hot 编码用categorical_crossentropy,要么保持整数标签换sparse_categorical_crossentropy。两者在数学上是一样的,只是接口不同。最怕的就是神经网络入门第一课在这里卡了半小时,实际上就是一行代码的差别。
5.3 解压后 main.py 打开全是乱码:文件编码的问题,不是文件坏了
homework6里的 doc 能正常打开,但 .py 文件在 macOS 或 Linux 上用文本编辑器打开显示中文注释全是乱码。原因很简单:Windows 下保存的 Python 文件默认是 GBK 编码,而 macOS/Linux 默认用 UTF-8 解析,两个编码不对付就显示成乱码。这不是文件损坏,Python 解释器也能正常跑,因为字符串字面量是 ASCII 码。解决方法是把文件编码转成 UTF-8:
# 用 iconv 从 GBK 转成 UTF-8,生成一个新文件再跑 iconv -f GBK -t UTF-8 main.py > main_utf8.py如果iconv报非法字符,说明文件里混了其他编码的字符,用 Python 的chardet库先检测:
import chardet with open('main.py', 'rb') as f: data = f.read() result = chardet.detect(data) print(result) # 输出编码类型和置信度还有更隐蔽的变种:代码能跑但中文输出全变成鍑鸿这种乱码,也是编码问题。建议拿到 .py 文件后第一件事就是检查文件编码,别等到跑出游荡乱码再排查。
5.4 Loss 出现 NaN 且准确率为 0:学习率太大,权重直接飞了
训练不到几个 epoch,loss 变成nan,准确率显示 0,整个训练直接废掉。原因是学习率设得过大,梯度更新一步跨太远,权重值爆炸到数值溢出。常见于用手写数字 MNIST 上的配置直接套 Fashion-MNIST,或者优化器从 SGD 换成 Adam 时没调整学习率。解决方法是:先把学习率降到现有值的十分之一,比如 0.001 降到 0.0001;同时检查数据里有没有 NaN 值,用np.isnan(x_train).any()快速排查。还有一个口诀留给新手:loss 是 nan,先降学习率,再看数据里有没有脏值,按这个顺序排查,95% 的情况能在五分钟内解决。
5.5 多次训练结果不一致,作业 report 没法写:随机种子没固定
第一次跑验证集准确率 91.2%,第二次跑变 90.8%,第三次变 91.5%,每次结果都不一样。如果你在 report 里只能写「准确率在 90%~92% 之间」,这份作业的严谨性直接打折扣。原因是神经网络的权重初始化、Dropout 的随机丢弃、数据打乱都是随机过程,GPU 上的并行计算还会引入额外的不确定性。解决方法是在程序最开头固定随机种子:
import numpy as np import tensorflow as tf import random # 固定三个层面的随机源:Python 内置、numpy、TensorFlow random.seed(42) np.random.seed(42) tf.random.set_seed(42) # 如果用了 GPU 还想更稳定,关闭 TF 的自动调优 tf.config.threading.set_inter_op_parallelism_threads(1) tf.config.threading.set_intra_op_parallelism_threads(1)固定种子之后,同一份代码在同样的硬件上跑两次结果应该完全一致。注意跨机器复现仍然可能因为 CUDA 版本、CPU 指令集差异产生微小偏差,但至少在你自己机器上能复现了,报告里的数字可写性大幅提高。
6. 实战落地技巧:把训练好的模型用起来,外加新的尝试方向
代码跑通了、准确率也拿到手了,作业提交之前还差一步——通常的流程是把测试集里的图片挑几张出来,让模型做预测,把结果可视化。这一项做好了,前面的模型部分就不像是个「完成就走」的作业,而是一个真正能用的分类器。核心是把训练时保存的best_model.h5加载回来,吃到新图片、输出每个类别的概率。切忌重新训练一遍模型再来预测,那样既慢又浪费。
from tensorflow.keras.models import load_model # 加载 checkpoint 保存的最佳权重 model = load_model('best_model.h5') # 取测试集前 5 张图做预测 sample_images = x_test_processed[:5] predictions = model.predict(sample_images) # argmax 拿到预测类别,同时打印每个类别的概率 class_names = ['T恤', '裤子', '套头衫', '连衣裙', '外套', '凉鞋', '衬衫', '运动鞋', '包', '踝靴'] for i in range(5): pred_class = np.argmax(predictions[i]) true_class = y_test_raw[i] confidence = np.max(predictions[i]) print(f"样本 {i}: 真实标签 {class_names[true_class]}, " f"预测 {class_names[pred_class]}, 置信度 {confidence:.4f}")这份代码里的predict返回的是 5×10 的二维数组,每一行是模型对该图片在 10 个类别上的置信度,np.argmax取最大值所在的下标作为预测类别。如果你想把可视化也做了,用matplotlib把图片和预测结果拼在一起输出,report 里直接插图。输出末尾增加一个attempt.py、在 doc 里加一段说明这些内容,其实就是「接下来还能怎么玩」的进阶。那之后我每跑一次带 CNN 的作业,都会强制自己走一遍「加载 checkpoint → 单张预测 → 可视化 confidences」这套流程,不这么做的话,模型的泛化能力到底如何,心里始终不踏实。一个模型如果训练时是神,测试时是鬼,那一定是在数据预处理和类别编码上出了岔子,别急着怀疑网络结构。希望这份拆解能帮你把 homework6 的代码从头到尾吃透,少走我当年走过的弯路。
本文还有配套的精品资源,点击获取