
简介面向机器学习初学者和需要完成图像分类实战作业的学生这份压缩包提供了基于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 到 2550 是纯黑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_classes10) y_test_onehot tf.keras.utils.to_categorical(y_test, num_classes10) print(f归一化后像素范围: {x_train.min():.2f} ~ {x_train.max():.2f}) print(fone-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_classes10必须和输出层神经元数量一致写少了训练时会直接报维度不匹配错误。如果不想用 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 卷积核paddingsame 保持空间尺寸 Input(shape(28, 28, 1)), Conv2D(32, (3, 3), activationrelu, paddingsame), MaxPooling2D((2, 2)), # 第二个卷积块64 个 3x3 卷积核特征通道翻倍 Conv2D(64, (3, 3), activationrelu, paddingsame), MaxPooling2D((2, 2)), # 展平后接全连接层 Flatten(), Dense(128, activationrelu), Dropout(0.3), # 输出层10 个神经元softmax 输出概率分布 Dense(10, activationsoftmax) ]) return model model build_cnn() model.summary()逻辑说明Input(shape(28, 28, 1))显式声明输入形状必须和预处理后的数据 shape 一致Conv2D(32, (3, 3), activationrelu, paddingsame)表示 32 个 3×3 卷积核paddingsame让卷积后特征图尺寸保持不变valid则会缩小MaxPooling2D((2, 2))把特征图宽高各减半28×28 经过两次池化变成 7×7Dropout(0.3)是正则化手段训练时随机丢弃 30% 的神经元防止过拟合输出层Dense(10, activationsoftmax)输出 10 类概率。参数调整的优先级先改卷积核数量32→64→128再调 Dropout0.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, activationrelu), Dense(10, activationsoftmax) ]) 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( optimizerAdam(learning_rate0.001), lossCategoricalCrossentropy(), metrics[accuracy] ) # 自动保存验证集准确率最高的权重防止跑完没存模型 checkpoint ModelCheckpoint( best_model.h5, monitorval_accuracy, modemax, save_best_onlyTrue, verbose1 ) # 验证集 loss 连续 5 轮不下降就停止省时间也防过拟合 early_stop EarlyStopping( monitorval_loss, patience5, restore_best_weightsTrue ) history model.fit( x_train, y_train_onehot, batch_size64, epochs30, validation_split0.2, # 从训练集里切 20% 做验证集 callbacks[checkpoint, early_stop], verbose1 )逻辑说明compile阶段把优化器、损失函数、评估指标绑定到模型上ModelCheckpoint在每轮 epoch 结束后对比验证集准确率只保存最好的权重文件这样即使训练中断也有后悔药EarlyStopping监测验证集 loss连续 5 轮不下降就提前终止训练restore_best_weightsTrue会把权重回滚到验证集最优的那一轮validation_split0.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], labeltrain_loss) ax1.plot(history.history[val_loss], labelval_loss) ax1.set_xlabel(Epoch) ax1.set_ylabel(Loss) ax1.legend() # 右边画准确率曲线 ax2.plot(history.history[accuracy], labeltrain_acc) ax2.plot(history.history[val_accuracy], labelval_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 size16、32、64、128batch 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_classes10) 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学习率太大权重直接飞了训练不到几个 epochloss 变成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 的代码从头到尾吃透少走我当年走过的弯路。本文还有配套的精品资源点击获取