神经网络从入门到精通 05:CNN初体验:手把手带你搭建时尚单品图像搜索引擎 - 教程

news/2025/11/21 16:18:24/文章来源:https://www.cnblogs.com/yangykaifa/p/19253483

CNN初体验:手把手带你搭建时尚单品图像搜索引擎

往期课程

  • 01:Anaconda+VSCode 2025年10月最新环境配置攻略

  • 02:[神经网络从入门到精通 2025年 ]:必要库安装+Pytorch配置

  • 03:[神经网络从入门到精通 2025]:首战手写字识别

  • 04:神经网络从入门到精通 04:“数据管线”的实战与拆解

CNN初体验:手把手带你搭建时尚单品图像搜索引擎

哈喽,大家好,我是小猿。先前课程终我们已经用一个基础的多层感知机(MLP)模型成功识别了手写数字,把 PyTorch 训练那一套基本流程跑通了。但你可能也发现了,MLP 在处理图像时,需要先把二维的图片“压扁”成一维向量,这会丢失宝贵的空间信息。

为了解决这个痛点,今天我们要学习一个专为图像而生的强大工具——卷积神经网络(Convolutional Neural Network, CNN)。咱们不只学理论,还要动手把它变成一个超酷的应用:一个能“以图搜图”的时尚单品搜索引擎!跟着这篇教程走完,你就能做出下面这样的效果:输入一张T恤的图片,它能帮你从一堆衣服里找出所有相似的款式。

开工前的准备

在开始之前,我默认你已经准备好了这些“装备”:

  • 熟悉 Anaconda、VSCode 以及终端的基本操作。

  • 对 PyTorch 的核心三件套 Tensor、Dataset、DataLoader 不陌生。

  • 最重要的是,你已经有过搭建并训练一个简单 MLP 模型的完整经验。

  • 我们这次用的数据集依然是 FashionMNIST,相信你已经知道怎么加载它了。

如果上面几点还有些生疏,强烈建议先回看我们之前课程关于MLP的内容哦。

小猿精心准备好了数据集+课件的下载链接:分享文件:Lession5.zip 链接:https://pan.xunlei.com/s/VOcQlR0hhbQL3FKpSfAOtaHOA1?pwd=nfg3# 复制这段内容后打开迅雷,查看更方便小猿这里精心制作了ipynb课件,方便大家复习和快速查阅!希望大家喜欢!解压密码是:chitaibao

项目蓝图与CNN初探

动手敲代码前,我们先在脑子里把这个“以图搜图”引擎的逻辑跑一遍。其实过程很简单,拢共分四步:

  1. 特征提取:用一个“聪明的模型”(也就是我们的CNN)给每一张图片生成一串独特的数字编码,这串编码就是它的“特征指纹”。

  2. 特征库构建:把我们数据集中所有图片的“特征指纹”都提取出来,存成一个库。

  3. 相似度计算:当用户上传一张新图片时,我们先提取它的特征指纹,然后拿这个指纹去特征库里跟所有指纹一一对比,计算相似度分数。

  4. 结果排序:把分数从高到低一排,分数最高的那些图片,就是我们要找的相似款。

那么问题来了,为什么提取图片“指纹”要用 CNN,而不是我们上节课学的 MLP 呢?

MLP 处理图片时,会把像素粗暴地拉成一条线,相邻的像素可能就分开了,完全没考虑图片里“一块一块”的结构。而 CNN 不一样,它通过“局部感受野”和“参数共享”两大绝招,像人眼一样先看局部(比如衣服的领口、袖子),再把这些局部特征组合起来理解整张图。这让它天生就更擅长处理图像任务。

CNN的核心部件:卷积层与池化层

要搭建 CNN,我们得先认识它的两个核心积木:卷积(Convolution)层和池化(Pooling)层。

卷积层:像拿着放大镜扫视图片

卷积层的工作,就像你拿着一个带网格的放大镜(这个放大镜就是过滤器 Filter,也叫卷积核 Kernel)在图片上一点点地移动扫描。

  • 过滤器(Filter/Kernel):一个小小的权重矩阵,它负责识别特定的局部特征,比如边缘、角点或者某种纹理。

  • 步长(Stride):就是放大镜每次移动的格子数。步长大,看得快,但可能漏掉细节。

  • 填充(Padding):在图片周围补上一圈“白边”(通常是0),这样可以保证即使是图片边缘的像素也能被过滤器扫到,同时还能控制输出图片的大小。

卷积运算之后,我们通常会紧跟一个激活函数(Activation Function),最常用的是 ReLU。它的作用很简单,就是给网络增加一些非线性,让它能学习更复杂的关系,就像给机器的大脑增加一些“灵活性”。

池化层:给特征图“瘦身”提纯

卷积层扫出了一堆特征图(Feature Map),但信息可能有点冗余,而且计算量大。这时候池化层就登场了,它的任务是给特征图降采样,也就是“瘦身”。

  • 最大池化(Max Pooling):在一个小区域里,只保留最强的那个特征信号(取最大值)。这相当于提取最显著的特征,同时对位置的一些小变动不太敏感。

  • 平均池化(Average Pooling):在一个小区域里,把所有特征信号取个平均值。

通过卷积和池化,输入图像的尺寸会一步步变小,但深度(通道数)会增加,这个过程就像把原始的像素信息,层层提炼成越来越抽象、越来越高级的语义特征。

小贴士:你可以自己动手算一算,一个 28x28 的输入图片,经过一个 5x5 的卷积核(步长1,无填充),再经过一个 2x2 的最大池化(步长2),输出的特征图尺寸会是多少?搞明白这个计算,你就掌握了 CNN 尺寸变化的核心。

动手搭建你的第一个CNN模型

环境准备与数据加载

理论聊得差不多了,是时候上代码了!我们会用 PyTorch 的 nn.Module 来搭一个简单的 CNN。第一步,就是把所有需要的“工具”(库)和我们已经下载好的“原材料”(本地数据集)都准备好。这个代码段就是我们的“备料区”。

  • 导入库:我们先请来几位“老师傅”:torch 和 torch.nn 是搭建神经网络的核心;torchvision 擅长处理图像数据;DataLoader 负责有条不紊地给我们“喂”数据;而 matplotlib 则是我们的“首席美术师”,负责把结果画出来给我们看。

  • read_idx_file 辅助函数:FashionMNIST的.gz文件是一种特殊的IDX格式。我们先创建一个“翻译器”函数,专门负责解压并读取这种格式的文件,把它转换成我们熟悉的numpy数组。 LocalFashionMNIST 自定义类:这是我们新的“专属数据管理员”。它继承了PyTorch的torch.utils.data.Dataset,并实现了三个核心方法:

  • init (初始化): 在创建实例时,它会根据我们提供的路径(data/fashion),直接找到对应的训练或测试文件,并调用read_idx_file函数把图像和标签数据一次性读入内存。

  • len (获取长度): 简单地告诉DataLoader我们的数据集中一共有多少张图片。

  • getitem (按索引取货): 这是最核心的部分。当DataLoader需要第 idx 张图片时,这个方法就会被调用。它从内存中取出对应的图像和标签,进行必要的格式转换(从numpy数组转为PIL图像,以便transform处理),最后应用我们定义好的数据转换流程,返回一对处理好的图像张量和标签。

  • 实例化与使用:最后,我们像使用官方数据集一样,简单地创建LocalFashionMNIST的实例,并把它传递给DataLoader。整个下游的可视化流程完全不用改变! 通过这个方法,我们实现了对数据加载流程的完全掌控,无论数据存放在什么奇特的路径下,我们都能轻松应对。

# Cell 1: Environment Setup and Data Loading (Custom Local Dataset)
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader, Dataset
from PIL import Image
import os
import gzip
import numpy as np
import matplotlib.pyplot as plt
import torch.nn.functional as F
# --- 1. 创建辅助函数,用于读取IDX格式文件 ---
def read_idx_file(path):
    """读取IDX格式的 .gz 文件并返回numpy数组"""
    with gzip.open(path, 'rb') as f:
        # 读取文件头信息
        magic = int.from_bytes(f.read(4), 'big')
        num_items = int.from_bytes(f.read(4), 'big')
        # 根据魔数判断是图像还是标签
        if magic == 2051: # 图像文件
            num_rows = int.from_bytes(f.read(4), 'big')
            num_cols = int.from_bytes(f.read(4), 'big')
            data = np.frombuffer(f.read(), dtype=np.uint8)
            data = data.reshape(num_items, num_rows, num_cols)
        elif magic == 2049: # 标签文件
            data = np.frombuffer(f.read(), dtype=np.uint8)
        else:
            raise ValueError(f"Unknown magic number {magic} in file {path}")
        return data
# --- 2. 创建自定义的数据集类 ---
class LocalFashionMNIST(Dataset):
    def __init__(self, root_dir, train=True, transform=None):
        self.transform = transform
        self.train = train
        if self.train:
            image_file = 'train-images-idx3-ubyte.gz'
            label_file = 'train-labels-idx1-ubyte.gz'
        else:
            image_file = 't10k-images-idx3-ubyte.gz'
            label_file = 't10k-labels-idx1-ubyte.gz'
        # 读取数据到内存
        self.images = read_idx_file(os.path.join(root_dir, image_file))
        self.labels = read_idx_file(os.path.join(root_dir, label_file))
    def __len__(self):
        return len(self.labels)
    def __getitem__(self, idx):
        image = self.images[idx]
        label = int(self.labels[idx]) # 确保标签是Python的int类型
        # 转换为PIL Image以适配torchvision的transforms
        image = Image.fromarray(image, mode='L')
        if self.transform:
            image = self.transform(image)
        return image, label
# --- 3. 使用我们的自定义类加载数据 ---
# 准备数据转换流程
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,)) # 归一化
])
# 指定你的 "fashion" 文件夹路径
local_data_path = 'data/fashion'
try:
    # 实例化我们自己的数据集类
    trainset = LocalFashionMNIST(root_dir=local_data_path, train=True, transform=transform)
    trainloader = DataLoader(trainset, batch_size=64, shuffle=True)
    testset = LocalFashionMNIST(root_dir=local_data_path, train=False, transform=transform)
    testloader = DataLoader(testset, batch_size=64, shuffle=False)
    print(f"从 '{local_data_path}' 目录加载本地数据集成功!")
except Exception as e:
    print(f"加载本地数据失败: {e}")
    print("\n请仔细检查:")
    print(f"1. 路径 '{local_data_path}' 是否正确。")
    print("2. 该文件夹内是否包含4个必需的 .gz 文件。")
# 类别标签
classes = ('T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',
           'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot')
# --- 可视化输出 ---
# 仅在数据加载成功时执行
if 'trainloader' in locals():
    print("下面是一些训练集中的样本:")
    # 获取一些随机的训练图片
    dataiter = iter(trainloader)
    images, labels = next(dataiter)
    # 定义一个函数来显示图片
    def imshow(img):
        img = img / 2 + 0.5     # 反归一化
        npimg = img.numpy()
        plt.figure(figsize=(8, 8))
        plt.imshow(np.transpose(npimg, (1, 2, 0)))
        plt.axis('off')
        plt.show()
    # 显示图片
    imshow(torchvision.utils.make_grid(images[:4]))
    # 打印标签
    print(' '.join(f'{classes[labels[j]]:12s}' for j in range(4)))

运行代码后我们能看到jupyter会展示几张样本图像以确认数据已准备就绪。

####定义并测试CNN模型

“原材料”备好了,现在我们要开始设计和搭建我们搜索引擎的“引擎核心”——卷积神经网络(CNN)。这个代码段就是我们的“模型设计图纸”。

  • SimpleCNN 类:我们用 class 关键字定义了一个新的模型,它继承自 PyTorch 的 nn.Module,这是所有模型的“基类”。

  • 核心积木:在 init 函数里,我们像搭积木一样定义了模型的每一层:
    • nn.Conv2d:这就是前面说的“带网格的放大镜”,负责扫描图片,提取局部特征。我们用了两层卷积,让模型能从简单的边缘、纹理学到更复杂的图案。

    • nn.ReLU:激活函数,给网络增加“灵活性”,让它能学习更复杂的模式。

    • nn.MaxPool2d:池化层,给特征图“瘦身”,保留最精华的信息。

    • nn.Flatten:高频出错点! 卷积和池化处理的是二维图像,但最后做决策的全连接层 nn.Linear 只接受一维的向量。Flatten 的作用就是把二维的特征图“压扁”成一维长条,做好交接工作。

    • nn.Linear:全连接层,它像一个“决策大脑”,根据前面提取到的特征,最终判断这张图片属于10个类别中的哪一个。

  • 模型测试:模型搭好后,我们不能盲目自信。我们创建一个 dummy_input (假的输入张量),把它送进模型里跑一圈。如果它能顺利“通过”所有层,并且输出的形状是我们预期的 torch.Size([1, 10])(代表1张图片,10个类别的得分),那就说明我们这个“引擎”的内部结构没问题,可以准备点火(训练)了!

import torch
import torch.nn as nn
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        # 第一个卷积-激活-池化层
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=16, kernel_size=5, stride=1, padding=2)
        self.relu1 = nn.ReLU()
        self.pool1 = nn.MaxPool2d(kernel_size=2)
        # 第二个卷积-激活-池化层
        self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5, stride=1, padding=2)
        self.relu2 = nn.ReLU()
        self.pool2 = nn.MaxPool2d(kernel_size=2)
        # 展平层,为全连接层做准备
        self.flatten = nn.Flatten()
        # 全连接层,用于分类
        self.fc = nn.Linear(32 * 7 * 7, 10) # FashionMNIST图片经过两次2x2池化后尺寸变为7x7
    def forward(self, x):
        x = self.conv1(x)
        x = self.relu1(x)
        x = self.pool1(x)
        x = self.conv2(x)
        x = self.relu2(x)
        x = self.pool2(x)
        x = self.flatten(x)
        x = self.fc(x)
        return x
# 让我们来测试一下模型
model = SimpleCNN()
# 创建一个假的输入张量,模拟一张28x28的灰度图
# (batch_size=1, channels=1, height=28, width=28)
dummy_input = torch.randn(1, 1, 28, 28)
output = model(dummy_input)
print(f"输入张量的形状: {dummy_input.shape}")
print(f"输出张量的形状: {output.shape}")

把这段代码跑一下,如果它能顺利打印出输入和输出张量的形状(输出应该是 torch.Size([1, 10])),并且不报错,恭喜你,你的第一个 CNN 模型已经成功搭好了!

注意:在 nn.Linear 之前,我们必须加一个 nn.Flatten() 层。这是个高频出错点!因为卷积层和池化层处理的是四维张量 (batch, channels, height, width),而全连接层只能接受二维张量 (batch, features)Flatten 的作用就是把后面三个维度“压扁”成一个维度。

训练CNN分类模型

引擎造好了,但它现在还是个“空壳”,什么也不懂。我们需要用数据来“教”它,让它学会识别不同的时尚单品。这个过程就是模型训练,俗称“炼丹”。

  • **criterion (损失函数)**:我们选择 nn.CrossEntropyLoss 作为“评分标准”。当模型预测错误时,它会给出一个较高的“惩罚分数”(loss),预测得越准,分数越低。

  • **optimizer (优化器)**:我们使用 optim.Adam 作为“优化教练”。它的任务是根据“惩罚分数”,微调模型内部的参数(权重),目标是让总分数越来越低。

  • **训练循环 (for epoch in range...)**:epoch 指的是把整个训练集从头到尾学习一遍。我们这里设置为3个epoch,意味着模型会把所有图片看3遍,每一遍都会比上一遍“学得更深”。

  • 监控学习进度:在循环内部,我们会定期打印出 loss 值。观察这个值的变化非常重要! 如果你看到 loss 在稳步下降,那就意味着模型确实在“学习进步”,我们的“炼丹”方向是对的。

# 注意:为了节省时间,这里只训练5个epoch作为演示。
# 在实际应用中,您可能需要训练更多epoch以获得更好的性能。
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
num_epochs = 3
print("开始训练模型...")
for epoch in range(num_epochs):
    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        inputs, labels = data
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
        if i % 200 == 199: # 每200个mini-batches打印一次
            print(f'[Epoch {epoch + 1}, Batch {i + 1:5d}] loss: {running_loss / 200:.3f}')
            running_loss = 0.0
print('训练完成!')
# 保存模型权重(可选)
# torch.save(model.state_dict(), 'fashion_mnist_cnn.pth')

当代码块运行结束并打印出“训练完成!”,我们的CNN模型就从一个“小白”成长为了一个能识别时尚单品的“专家”了。

从分类器到特征提取器

模型训练好了,它现在很会“分类”。但我们的目标是“搜索”!怎么办呢?这里我们要来一次巧妙的“变身”。一个好的分类模型,其中间层必然已经学会了如何提取图像中最有代表性的特征。我们要做的是,对模型进行一次“小手术”,把最后做决策的“嘴巴”(全连接分类层)去掉,只保留它善于观察的“眼睛”和“大脑”(卷积、池化层)。

  • FeatureExtractor 类:这个新模型专门用于提取特征。它的构造非常聪明,直接“借用”了我们刚刚训练好的 trained_model。

  • “手术”过程:nn.Sequential(*list(original_model.children())[:-1]) 是实现这次“手术”的关键代码。它复制了原模型除了最后一层之外的所有结构,从而创建了一个纯粹的特征提取网络。

  • 保留 flatten:别忘了,我们仍然需要 flatten 层,因为它负责将卷积层输出的二维特征图整理成一维的向量形式,也就是我们想要的“特征指纹”。

  • 测试输出:我们再次用一个假数据来测试这个新的extractor。这次,你会看到输出的形状不再是 [1, 10],而是一个长长的向量,比如 torch.Size([1, 1568])。这 1568 个数字,就是这张图片被模型“理解”后,浓缩成的精华——独一无二的特征向量

class FeatureExtractor(nn.Module):
    def __init__(self, original_model):
        super(FeatureExtractor, self).__init__()
        # 修正:我们只取到flatten层之前的所有层(即所有卷积和池化层)
        self.feature_layers = nn.Sequential(*list(original_model.children())[:-2])
        # 单独保留flatten层
        self.flatten = original_model.flatten
    def forward(self, x):
        # 修正:先通过所有卷积/池化层
        x = self.feature_layers(x)
        # 然后再进行展平操作
        x = self.flatten(x)
        return x
# 假设我们已经训练好了一个SimpleCNN分类模型
trained_model = model # 直接使用上一段训练好的模型
trained_model.eval() # 设置为评估模式
# 创建特征提取器
extractor = FeatureExtractor(trained_model)
print("特征提取器创建成功!")
# --- 输出测试结果 ---
# 再次用假数据测试
dummy_input = torch.randn(1, 1, 28, 28)
feature_vector = extractor(dummy_input)
print(f"提取出的特征向量形状 : {feature_vector.shape}")

构建我们的“指纹库”

现在我们有了一个能为单张图片生成“指纹”(特征向量)的强大工具 extractor。但要实现搜索,我们必须先为图库里的所有图片都生成并存好指纹,建立一个完整的“指纹档案库”。

  • create_feature_library 函数:这个函数就是我们的“档案管理员”。它的工作是遍历 testloader 中的每一批图片。

  • **with torch.no_grad()**:这是一个聪明的优化。因为我们只是在提取特征,不需要计算梯度或进行学习,所以用这个语句可以告诉PyTorch“别费力气算梯度了”,从而大大提高运行速度,节省显存。

  • 批量处理:函数会一批一批地把图片送入extractor,得到一批批的特征向量,然后把它们都存进一个列表 features_list 里。

  • torch.cat:最后,我们用 torch.cat 把列表中所有批次的特征向量“拼接”起来,形成一个巨大的张量。这个张量就是我们的“特征库”!

  • 输出验证:代码运行结束后,会打印出这个特征库的形状。例如 torch.Size([10000, 1568]),这表示我们的测试集里有10000张图片,每张图片都对应一个1568维的特征向量。至此,搜索引擎的“后端数据库”已经准备就绪!

def create_feature_library(loader, model_extractor):
    features_list = []
    labels_list = []
    model_extractor.eval() # 确保模型在评估模式
    with torch.no_grad(): # 在此模式下,不计算梯度,节省计算资源
        for images, labels in loader:
            feature_vectors = model_extractor(images)
            features_list.append(feature_vectors)
            labels_list.append(labels)
    # 将列表中的张量合并成一个大张量
    all_features = torch.cat(features_list, dim=0)
    all_labels = torch.cat(labels_list, dim=0)
    return all_features, all_labels
print("正在为测试集所有图片构建特征库...")
feature_library, library_labels = create_feature_library(testloader, extractor)
# --- 输出特征库信息 ---
print("特征库构建完成!")
print(f"特征库张量的形状: {feature_library.shape}")
print(f"标签库张量的形状: {library_labels.shape}")

见证奇迹!实现并可视化图像搜索

万事俱备,只欠东风!我们有了“特征提取器”,也有了“特征指纹库”,现在是时候把它们组装起来,打造一个真正能用的搜索引擎,并亲眼见证搜索结果了!

  • find_similar 函数:这是搜索引擎的“匹配核心”。
    • F.normalize:在计算相似度之前,先对查询向量和特征库进行归一化。这是计算余弦相似度的关键一步,它能确保我们比较的是特征的“模式”(方向),而不是它们的“强度”(大小),结果会准得多。

    • torch.matmul:通过一次高效的矩阵乘法,瞬间计算出查询图片与库中所有图片的余弦相似度分数。

    • torch.sort:将相似度分数从高到低排序,我们就得到了最相似图片的索引。

  • 可视化流程
    1. 随机挑选查询图:我们从测试集中随便选一张图片作为我们的搜索目标。

    2. 提取查询指纹:用 extractor 为这张图片生成它自己的特征向量。

    3. 执行搜索:调用 find_similar 函数,在特征库中找到与查询指纹最“情投意合”的几个结果。

    4. 展示结果:最后,我们用 matplotlib 画出一个清晰的对比图:左边是我们的查询图片,右边是搜索引擎找到的最相似的5个结果。

def find_similar(query_vector, feature_lib):
    # 归一化,这是计算余弦相似度的标准步骤
    query_vector = F.normalize(query_vector, p=2, dim=1)
    feature_lib = F.normalize(feature_lib, p=2, dim=1)
    # 计算余弦相似度 (等价于归一化后的向量做矩阵乘法)
    similarities = torch.matmul(feature_lib, query_vector.T).squeeze()
    # 排序,返回得分和索引
    sorted_scores, sorted_indices = torch.sort(similarities, descending=True)
    return sorted_scores, sorted_indices
# --- 可视化搜索结果 ---
# 1. 随机选择一张查询图片
query_idx = np.random.randint(0, len(testset))
query_image, query_label = testset[query_idx]
query_image_tensor = query_image.unsqueeze(0) # 增加一个batch维度
# 2. 提取查询特征
query_vector = extractor(query_image_tensor)
# 3. 执行搜索
scores, indices = find_similar(query_vector, feature_library)
top_k = 6 # 查找最相似的6张图 (第一张通常是它自己)
# 4. 显示结果
fig, axes = plt.subplots(1, top_k, figsize=(15, 3))
# 显示查询图片
ax = axes[0]
query_img_display = query_image.squeeze().numpy() / 2 + 0.5
ax.imshow(query_img_display, cmap='gray')
ax.set_title(f"Query:\n{classes[query_label]}")
ax.axis('off')
# 显示搜索结果
for i in range(1, top_k):
    ax = axes[i]
    result_idx = indices[i].item()
    result_image, result_label = testset[result_idx]
    result_img_display = result_image.squeeze().numpy() / 2 + 0.5
    ax.imshow(result_img_display, cmap='gray')
    ax.set_title(f"Result {i}\n{classes[result_label]}")
    ax.axis('off')
plt.suptitle("时尚单品图像搜索引擎结果", fontsize=16)
plt.show()

见证奇迹:运行这个代码块,你将看到最终的可视化输出。如果一切顺利,你会发现搜索到的图片不仅类别相同(比如都是鞋子),甚至在款式、颜色、角度上都和查询图片有几分神似。恭喜你,你已经成功搭建了一个属于自己的时尚单品图像搜索引擎!!

收尾与展望

走到这里,你手上应该有了一个完整的 Notebook,里面包含了从模型定义、特征提取到最终搜索和可视化的全部代码。这个小项目虽然简单,但它完整地展现了深度学习在计算机视觉领域解决实际问题的核心思路。

感觉不过瘾?这里有两个小挑战你可以试试:

  1. 换个“大脑”:我们现在用的是自己搭的简单 CNN。你可以试试用一个更强大、别人已经训练好的模型(比如 ResNet-18,我们下节课会讲)来当特征提取器,看看搜索效果会不会有质的飞跃。

  2. 评估效果:尝试对不同类别的物品(比如鞋子、包、裤子)进行搜索,主观感受一下哪些类别的效果好,哪些不好,并思考一下可能的原因。

下一节课,我们将正式进入一个激动人心的话题——迁移学习。你将学会如何站在巨人的肩膀上,利用那些在大规模数据集上预训练好的模型,用极少的代码和算力,在自己的任务上达到惊人的效果。

在下一节中,我们将深入这片“炼丹”的核心区域,亲手揭开超参数的神秘面纱!你将学会如何像一位经验丰富的工程师一样,系统地调校你的模型,找到那组能让其性能最大化的“黄金参数”。我们还将引入验证集这一至关重要的工具,它将成为你的“仪表盘”,实时监控模型的学习状态,并教会你如何精准识别并对抗深度学习中最臭名昭著的敌人——过拟合!

我们下期见!

常见问题答疑

Q1:我的模型训练时,损失(loss)一直不下降怎么办? A:这通常是“炼丹”老大难问题。先别慌,检查两件事:一是学习率是不是太高或太低了?可以尝试调大或调小10倍试试。二是确认你的优化器(Optimizer)已经正确关联了模型的参数,并且输入数据已经做了归一化处理。

Q2:一运行就报 CUDA out of memory 错误? A:这是“爆显存”了。最直接的原因就是你的批次大小(Batch Size)设置得太大了,显卡装不下。去 DataLoader 里把 batch_size 调小一半,比如从64降到32,通常就能解决。如果还不行,那可能是模型本身太复杂了,需要考虑简化模型结构。

Q3:为什么我的搜索结果风马牛不相及? A:这说明模型提取的特征没有区分度。主要原因有两个:一是模型没训练好,在分类任务上的准确率本身就很低,所以它根本没学到东西。回去增加训练轮次(Epochs),先把分类准确率提上来。二是在计算余弦相似度前,可能忘记对特征向量做归一化(normalize),这一步很关键,可以大大提升效果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/972258.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

南京市一对一家教机构怎么选?2026年五大机构测评榜出炉!

在教育竞争激烈的当下,南京家长对孩子的学业关注度持续升高,一对一辅导“因材施教”的优势,成为不少家庭补弱提优的首选。无论是应对中小学阶段的知识难点,还是冲刺高考的关键复习期,优质的一对一机构都能为孩子提…

Day28、29:2025年10月18日、19日,周末,休息。

周末主题就是会友,感谢何哥前段时间的鼎力协助,把珍藏的习酒君品拿出来与君同饮,与君同销万古愁,哈哈哈哈哈哈。周末还去户外开炉子,点燃冬天的第一把火,当整个炉子呈现紫金色的时候,是真的好好看。这东西就跟烧…

CF2165F Arctic Acquisition 题解

Description 给定一个长度为 \(n\) 的排列 \(a_1, a_2, \dots, a_n\)。 一个区间 \([l, r]\) 是锯齿形的,当且仅当该区间包含一个 21435 子序列;即存在整数 \(i_1, i_2, i_3, i_4, i_5\),满足 \(l \leq i_1 < i_…

2025年什么产品能有效淡化纹路?全肤质淡纹抗老方案出炉

什么产品能有效淡化纹路?这是2025年抗衰护肤领域绕不开的核心问题。纹路的产生,本质是真皮层胶原流失、弹性纤维断裂,而普通护肤品大多困于角质层屏障,成分难以深入真皮层发挥作用。想要高效淡纹抗老,需从透皮吸收…

2025年11月天津线缆厂家名单:天津中压、变频、聚乙烯绝缘电缆生产厂家TOP10推荐

在工业生产与城市建设的电力传输体系中,中压电缆、变频电缆及聚乙烯绝缘电缆作为核心载体,其性能稳定性、安全可靠性直接决定项目的整体质量与运行效率。天津凭借完善的工业产业链、先进的制造工艺与严格的质量管控体…

redhat 9.3 安装oracle 19

1、关闭防火墙systemctl stop firewalld.service systemctl disable firewalld2、关闭seLINUXsed -i s/^SELINUX=enforcing/SELINUX=disabled/ /etc/selinux/config3、安装所需的包yum install -ybc \binutils \compat…

2025年哈尔滨心理咨询学校权威推荐榜单:特殊教育/早教中心/口肌训练源头学校精选

在心理健康服务需求持续增长的推动下,哈尔滨心理咨询服务市场呈现专业化、细分化的趋势。据行业数据显示,2025年哈尔滨心理咨询服务需求量同比去年增长42%,其中情绪管理、亲子关系、职业倦怠和婚恋问题成为四大主要…

Day30:2025年10月20日,星期一,值班,诸事皆顺。

不知不觉过来都满月了,昨天晚上三女一男illegal use of toxic substances嗨到凌晨,上个班留给我们处理,本可以早早收工,奈何有人添乱,真的是服了。今天下午又抓了一伙扯旋儿的,前段时间还说没什么强度,看来这是…

收集飞花令碎片——C语言内存函数 - 实践

收集飞花令碎片——C语言内存函数 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "M…

绍兴一对一课外辅导机构推荐:2025年综合适配度排行榜

越城区的小学家长愁孩子数学应用题总卡壳,柯桥区的初中家长为物理难点补习犯难,诸暨市的高中家长急着找高考冲刺的一对一家教,就连上虞区、嵊州市、新昌县的各学段家长,也都在为选课外辅导机构或平台头疼不已! 地…

绍兴一对一家教辅导机构推荐:2025权威测评排行榜,第一个性价比最高

“孩子数学压轴题总卡壳,换了两家一对一辅导机构都没效果,靠谱的小学、初中、高中课外辅导平台到底在哪找?”这位来自越城区家长的焦虑,在柯桥区、上虞区、诸暨市,乃至嵊州市、新昌县的家长中极具代表性——毕竟小…

天门一对一家教机构终极推荐:2026最新辅导机构口碑TOP榜单!真实反馈闭眼选

“给孩子选一对一补习平台,比给自己找工作还难!” 这是天门竟陵街道家长王女士最近的感慨。她的孩子上初二,数学成绩一直在及格线徘徊,两个月内试了 3 家机构,要么老师讲课照本宣科,要么课后没人管,钱花了不少,…

计算机视觉:YOLO实现目标识别+目标跟踪技术 pyqt界面 OpenCV 计算机视觉 深度学习 计算机(建议收藏)✅ - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025 年电线电缆厂家 TOP 企业品牌推荐排行榜11月更新:消防 / 耐火 / 防火/ 阻燃 / 阻燃B1级 / 矿物质防火/ 柔性防火 / 低烟无卤火电缆电线推荐!

在工业生产升级与民生基建加速的当下,电线电缆作为能量传输与信号传递的核心载体,其质量稳定性与性能适配性直接决定了工程安全与运行效率。当前市场中,电线电缆企业数量庞大,但部分中小型厂家存在原材料以次充好、…

潜江一对一课外辅导机构推荐,2026最新家教机构排行榜:靠谱不踩坑指南

潜江园林街道、泽口街道的家长,或是广华寺街道、周矶街道的朋友,还有杨市街道、泰丰街道以及高石碑镇、熊口镇、老新镇、龙湾镇的宝爸宝妈们,乃至张金镇、浩口镇、积玉口镇、渔洋镇、王场镇、竹根滩镇的乡亲们,是不…

Python的类对象、实例对象、类属性、实例属性、类方法、实例方法

Python的类对象、实例对象、类属性、实例属性、类方法、实例方法原文链接: https://blog.csdn.net/qq_44154915/article/details/134047553简易理解(快速理解)类对象:定义的类就是类对象 实例对象:类对象实例化后就是…

潜江一对一课外补习机构推荐:2026 最新教育机构天花板榜单!提分快还省钱

孩子语文阅读理解总丢分,数学公式记不牢,英语单词背了就忘,物理电学摸不着头脑,化学方程式不会配平,史地政知识点记不住?潜江家长给孩子找一对一辅导,是不是总陷入这样的纠结?“潜江城区、广华寺街道、浩口镇、…

2026年池州一对一家教机构推荐:五大辅导机构测评排行榜,综合实力全解析!

池州家长为孩子选小学、初中、高中一对一家教辅导时,盼从口碑排名靠谱的教育机构中,找兼顾课外补习与升学培优的培训选择 —— 既想靠针对性补课填知识缺口、跟校内进度,也盼借系统辅导提学习能力、为升学铺路,却常…

UVA1437 String painter 分析

题目概述 给定字符串 \(A\) 和字符串 \(B\),定一次操作为将 \(A\) 一个区间的字符全部换成同一个,问最小操作让 \(A\rightarrow B\)。 分析 一看完题目感觉似曾相识,好像有道题目类似吧。 就这道:P4170 [CQOI2007]…

Ubuntu22.04.4安装配置CUDA12.5,Cdnn官方详细版本

​安装需求如下图机器raid配置 两块磁盘做raid1,参见官方raid,配置手册https://www.supermicro.com/support/manuals/ 系统下载 https://old-releases.ubuntu.com/releases/22.04/ 制作U盘使用rufus制作,U盘系统安装…