简介:这份文档面向具备一定机器学习与计算机视觉基础的学生和研究者,围绕2024年秋季机器学习期末竞赛,提供一份可落地的图像分类赛题指南,核心任务是依据古代中国书法与绘画的扫描图像预测作品所属朝代,并识别其中的AI生成图像。内容涵盖赛题背景、任务形式化定义、数据集构成(3600张512×512图像,含900张AI生成图)、类别标签体系(唐、宋、元、明、清及AI六类),以及数据清洗、类别不均衡与噪声标签处理等关键环节。评估指标部分详细说明总体准确率、非AI类别F1、AI类别F1与权益共享评分的加权方式,并给出提交规则、时间节点与注意事项。资源包为1个docx文档,压缩后约554KB,已有67人学习。读者可借此理解从数据预处理、模型训练到Kaggle提交的完整竞赛流程,掌握长尾与噪声场景下的优化思路,适合作为历史文物鉴定方向的项目实战参考。
1. 从一张青铜器照片,看懂朝代预测这条技术路线
你手里有一张博物馆拍的青铜器照片,或者一张古画的高清扫描图,能不能让模型直接告诉你它大概属于哪个朝代?这就是「基于图像识别的中国古代艺术品朝代预测」要解决的问题。它本质是一个细粒度图像分类任务:输入是艺术品的图像,输出是朝代标签,比如商周、秦汉、唐宋、明清。听起来像普通的图像分类,但真正动手你会发现,朝代之间的视觉差异远比猫狗分类微妙——同样是瓷器,宋代的青瓷和明代的青花,颜色、纹饰、器型都不同,可一旦图像质量差、角度偏,模型很容易翻车。这个方向适合两类人:一是做机器学习课程设计、比赛选题的学生,想找一个有文化厚度又能落地的题目;二是对图像识别有基础、想练细粒度分类的工程师。接下来我会把从数据准备到模型调参的完整路径拆开,让你能照着复现,也能看清哪里容易踩坑。
2. 朝代预测的数据从哪来:采集、清洗与标签体系
2.1 为什么公开数据集几乎找不到现成的
中国古代艺术品朝代预测不像 ImageNet 那样有现成的标注数据。常见做法是去博物馆官网、数字文物库、开放版权图库抓取图像,或者用比赛主办方提供的数据集。我一般会先确认三件事:图像是否允许用于研究、朝代标签是否明确、每个朝代的样本量是否均衡。很多开放数据只给了「年代」字段,比如「公元前 13 世纪」,你需要自己映射到朝代。这里有个血泪经验:不要直接用「夏商周」这种粗粒度标签,因为商和西周的艺术品风格差异很大,模型学不到区分度。建议至少分成商、西周、春秋战国、秦、汉、唐、宋、元、明、清十个类别,如果样本不够,可以合并成「先秦」「秦汉」「魏晋南北朝」「隋唐」「宋元」「明清」六个大类。
2.2 用 Python 做图像去重和尺寸归一化
抓下来的图往往有重复、水印、尺寸不一。下面这段脚本做三件事:用感知哈希去重、统一缩放到 224×224、按朝代分文件夹存放。
import os import cv2 import imagehash from PIL import Image from tqdm import tqdm def deduplicate_and_resize(src_dir, dst_dir, hash_threshold=5): seen_hashes = {} for dynasty in os.listdir(src_dir): dynasty_path = os.path.join(src_dir, dynasty) if not os.path.isdir(dynasty_path): continue save_path = os.path.join(dst_dir, dynasty) os.makedirs(save_path, exist_ok=True) for img_name in tqdm(os.listdir(dynasty_path)): img_path = os.path.join(dynasty_path, img_name) try: img = Image.open(img_path).convert('RGB') except: continue # 计算感知哈希 phash = imagehash.phash(img) # 检查是否与已有图像过于相似 duplicate = False for h in seen_hashes.get(dynasty, []): if phash - h < hash_threshold: duplicate = True break if duplicate: continue seen_hashes.setdefault(dynasty, []).append(phash) # 统一缩放到 224x224,保持长宽比用 padding img_resized = img.resize((224, 224), Image.LANCZOS) img_resized.save(os.path.join(save_path, img_name)) if __name__ == '__main__': deduplicate_and_resize('./raw_data', './clean_data')逻辑说明:imagehash.phash生成 64 位感知哈希,hash_threshold=5表示汉明距离小于 5 就视为重复。这个阈值可以调,设太小去重不干净,设太大可能误删不同朝代的相似器物。缩放时直接 resize 会变形,如果器型对分类重要,建议用 padding 补成正方形再缩放。参数方面,src_dir下每个子文件夹是一个朝代,dst_dir会生成同样的结构。跑完以后检查一下每个朝代的图片数量,如果某个朝代少于 200 张,后面训练时要做数据增强或者类别加权。
2.3 标签映射表怎么设计才不坑模型
标签映射不是简单地把朝代名转成数字。你需要考虑朝代的时间顺序,因为有些模型(比如有序分类)可以利用顺序信息。下面是一个映射表示例:
| 原始年代 | 粗粒度标签 | 标签 ID | 样本量参考 |
|---|---|---|---|
| 商代 | 先秦 | 0 | 300+ |
| 西周 | 先秦 | 0 | 300+ |
| 春秋战国 | 先秦 | 0 | 300+ |
| 秦代 | 秦汉 | 1 | 200+ |
| 汉代 | 秦汉 | 1 | 400+ |
| 唐代 | 隋唐 | 2 | 500+ |
| 宋代 | 宋元 | 3 | 600+ |
| 元代 | 宋元 | 3 | 300+ |
| 明代 | 明清 | 4 | 500+ |
| 清代 | 明清 | 4 | 500+ |
注意:如果比赛要求细粒度分类,就不要合并。合并只适用于样本极少的情况。另外,标签 ID 最好按时间顺序递增,这样即使模型输出连续值,也能通过阈值判断朝代。
3. 用迁移学习搭一个能跑的朝代分类模型
3.1 为什么选 ResNet50 而不是自己造网络
朝代预测的数据量通常不大,自己从零训练卷积网络很容易过拟合。常见做法是用在 ImageNet 上预训练的 ResNet50 或 EfficientNet,冻结底层卷积层,只训练最后的全连接层。我一般会先跑一个 baseline:ResNet50 + 全局平均池化 + 一个 5 类全连接层。如果准确率低于 50%,再考虑解冻部分层做微调。选 ResNet50 的理由是它结构简单、预训练权重容易获取、对细粒度分类也有不错的表现。EfficientNet 更轻量,但输入分辨率要求更高,训练时间反而可能更长。
3.2 完整训练脚本与关键参数
下面是一个基于 PyTorch 的训练脚本,包含数据加载、模型定义、训练循环和验证。
import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms, models # 数据增强:训练集用随机裁剪和翻转,验证集只做缩放 train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_dataset = datasets.ImageFolder('./clean_data/train', transform=train_transform) val_dataset = datasets.ImageFolder('./clean_data/val', transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4) # 加载预训练 ResNet50 model = models.resnet50(pretrained=True) # 冻结所有卷积层 for param in model.parameters(): param.requires_grad = False # 替换最后的全连接层 num_classes = len(train_dataset.classes) model.fc = nn.Linear(model.fc.in_features, num_classes) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) criterion = nn.CrossEntropyLoss() # 只优化全连接层参数 optimizer = optim.Adam(model.fc.parameters(), lr=1e-3) for epoch in range(20): model.train() running_loss = 0.0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() # 验证 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print(f'Epoch {epoch+1}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {correct/total:.4f}')逻辑说明:pretrained=True会下载 ImageNet 预训练权重。冻结卷积层后,只有model.fc的参数会被更新。学习率设 1e-3 是因为只训练全连接层,可以稍大。如果验证准确率在 5 个 epoch 后不再提升,可以解冻layer4并调小学习率到 1e-4。batch_size=32在 8GB 显存下比较安全,如果显存不够改成 16。num_workers=4根据 CPU 核心数调整。注意:RandomResizedCrop的scale=(0.8, 1.0)不要设得太小,否则会把器物局部裁掉,导致模型学不到整体器型。
3.3 类别不平衡时用加权损失和采样器
如果某个朝代的样本量只有其他朝代的十分之一,模型会偏向多数类。两种常见做法:一是在CrossEntropyLoss里传weight参数,权重设为样本量的倒数;二是用WeightedRandomSampler让每个 batch 里各类别比例均衡。我一般先用加权损失,因为它改动小。计算权重的代码:
from collections import Counter import numpy as np labels = [s[1] for s in train_dataset.samples] class_counts = Counter(labels) total = sum(class_counts.values()) weights = [total / class_counts[i] for i in range(num_classes)] class_weights = torch.FloatTensor(weights).to(device) criterion = nn.CrossEntropyLoss(weight=class_weights)参数说明:class_counts统计每个类别的样本数,weights是每个类别的权重,样本越少权重越大。注意权重不要设得过于极端,否则模型会对少数类过拟合。如果某个类别样本少于 50 张,建议直接合并到相邻朝代。
4. 避坑与排查:朝代预测里最容易翻车的五件事
4.1 验证集准确率很高,测试集一塌糊涂
现象:训练时验证集准确率到 85%,但比赛提交后只有 40%。原因:验证集和训练集来自同一批数据,图像风格、背景、拍摄角度都很相似,模型学到了背景而不是器物特征。解决:划分验证集时按来源分组,比如用博物馆 A 的图做训练,博物馆 B 的图做验证。如果做不到,至少要做 5 折交叉验证,并且用 Grad-CAM 可视化模型关注区域,确认它看的是器物而不是背景。
4.2 模型把「朝代」和「材质」搞混
现象:所有青铜器都被预测成先秦,所有瓷器都被预测成明清。原因:数据集中青铜器大多来自先秦,瓷器大多来自明清,模型学到了材质和朝代的虚假关联。解决:检查每个朝代下的材质分布,如果某个朝代只有一种材质,要么补充其他材质的样本,要么在训练时做材质平衡。另一个办法是多任务学习,同时预测朝代和材质,让模型解耦这两个特征。
4.3 图像预处理和预训练模型不匹配
现象:加载预训练权重后,loss 不下降。原因:自己写的归一化参数和 ImageNet 的不一致,或者输入通道数不对。解决:直接用transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),这是 ResNet 预训练时的标准参数。如果图像是灰度图,要复制成三通道。另外检查ToTensor()是否把像素值转到了 [0,1],不要重复除以 255。
4.4 数据增强把关键特征裁掉了
现象:训练准确率波动很大,验证准确率上不去。原因:RandomResizedCrop的 scale 设得太小,或者RandomRotation角度太大,把器物的口沿、底足裁掉了。解决:对于艺术品图像,建议用RandomResizedCrop(224, scale=(0.9, 1.0)),只做轻微裁剪。旋转角度控制在 ±10 度以内。颜色抖动也不要太强,因为朝代特征有时依赖特定釉色。
4.5 显存溢出和训练速度慢
现象:训练到一半报 CUDA out of memory。原因:batch_size太大,或者没有用torch.no_grad()包住验证循环。解决:先把batch_size降到 16,如果还不行就降到 8。验证时一定要用with torch.no_grad():,否则会保存计算图。另外,num_workers设得太高可能导致 CPU 瓶颈,一般设为 CPU 核心数的一半。如果图像尺寸是 448×448,显存占用会翻倍,建议先用 224×224 跑通再考虑放大。
5. 把准确率再往上推:微调策略与结果验证
5.1 解冻 layer4 做微调,学习率怎么设
当全连接层训练收敛后,可以解冻 ResNet50 的layer4做微调。这一步能让准确率提升 5 到 10 个百分点,但学习率必须调小。我一般用分层学习率:layer4用 1e-4,fc用 1e-3。代码改动如下:
# 解冻 layer4 for param in model.layer4.parameters(): param.requires_grad = True # 分层学习率 optimizer = optim.Adam([ {'params': model.layer4.parameters(), 'lr': 1e-4}, {'params': model.fc.parameters(), 'lr': 1e-3} ])注意:解冻后要重新跑验证,如果验证准确率下降,说明学习率太大,调成 5e-5 再试。微调时建议用余弦退火调度器,让学习率逐渐降到 0。
5.2 用混淆矩阵找出最容易混淆的朝代对
训练完模型后,不要只看总体准确率。画一个混淆矩阵,看看哪些朝代之间容易混。比如唐宋之间、明清之间。下面是一个画混淆矩阵的代码片段:
from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) cm = confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annot=True, fmt='d', xticklabels=train_dataset.classes, yticklabels=train_dataset.classes) plt.show()如果发现宋和元混淆严重,可以针对这两个朝代补充样本,或者训练一个二分类器专门区分它们。另一个技巧是使用标签平滑(label smoothing),把硬标签变成软标签,能缓解过拟合。
5.3 一个我常用的验证习惯
每次提交比赛结果前,我会留出 10% 的训练数据作为「本地测试集」,并且确保这个测试集和验证集没有重叠。然后用模型预测这批数据,人工检查预测错误的图像。很多时候你会发现,错误不是因为模型差,而是因为标签本身有问题——比如一张清代仿宋的瓷器被标成了宋。这种标签噪声在艺术品数据里很常见。我的习惯是:如果一张图连我都分不清朝代,就直接从训练集里删掉。宁可少几百张图,也不要让模型学错。这个方向值得做,因为朝代预测不仅能用在比赛里,还能扩展到文物检索、展览导览、修复辅助。希望帮到你。
本文还有配套的精品资源,点击获取