ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

电商产品图片分类数据库:从迁移学习到向量检索的完整实践

电商产品图片分类数据库:从迁移学习到向量检索的完整实践 做电商的朋友应该都有体会SKU一多商品图片就成了“失控的资产”。不同类目、不同批次、不同运营上传的图命名随意、存放散乱想统计“到底哪个类目图片最多”“有没有重复图占空间”全靠人肉翻文件夹。我做电商后台开发四年最头疼的就是给图片建索引——既要把图片文件管起来又得让业务系统能按“视觉内容”去查。后来我花了两周时间基于开源模型和数据库搭了一套“电商产品图片分类数据库”把图片分类和元数据管理整合到一个体系里终于治好了这个顽疾。这套东西能做什么呢简单说上传一张商品图系统自动识别它属于什么类目把图片路径、类目标签、特征向量同步写入数据库后面想按类目筛选、以图搜图、查重复图都能直接走接口。对做电商后台、商品运营、数据团队的读者来说这篇的完整设计与踩坑记录应该都能直接用上。下面我从设计思路、数据库建设、模型训练、实操落地和问题排查五个部分展开把我试过的方法、踩过的坑、最后沉淀下来的方案都写清楚。1. 电商产品图片分类的整体设计思路1.1 为什么专门给图片建数据库很多人一开始不理解图片不就是一个文件路径嘛存在磁盘上数据库里存个URL不就行了真做电商业务就知道不行。商品图片除了文件本身还关联着很多关键属性类目、品牌、适用季节、审核状态、是否主图、是否白底图甚至还有图片指纹用于查重。这些属性如果散落在文件名或者Excel里后续做筛选、统计、权限控制都非常痛苦。正经的做法是把图片当成“数据资产”来管理图片文件放对象存储或本地磁盘图片的元数据路径、标签、业务关联ID、上传时间放进关系型数据库图片的视觉特征用于以图搜图、相似推荐放进向量数据库。这样拆开之后每一层各司其职既能按业务字段精确筛选又能按图像内容模糊搜索还能通过SQL做聚合分析。这套架构我现在用在商品主图质检、竞品图片监控、运营素材管理等多个场景都跑得很稳。1.2 技术选型传统方案与深度学习方案对比先说传统方案。早些年我做图片分类用的是人工打标加规则引擎按上传目录命名分类或者通过图片颜色直方图、SIFT特征做简单聚类。这套方案在小规模下能用但问题也很明显运营人员手工打标容易漏规则分类遇到“苹果手机”和“苹果水果”这种同名不同类就傻眼更别说跨类目的相似商品图片了。现在的方案基本都是深度学习。我实际用的是迁移学习拿一个在大规模图像数据集上预训练好的模型比如ResNet、EfficientNet在自家商品图片数据集上做微调。拿“训练四分类花卉模型玫瑰图片”这件事举例四分类就是玫瑰、向日葵、郁金香、菊花用几千张标注好的图片微调一个预训练模型很快就能达到90%以上的准确率。电商类目识别跟这个思路完全一样只是把类别从花卉换成你的商品类目比如“女装-连衣裙”“3C数码-手机壳”“美妆-口红”模型能学会从图片中提取类目相关的视觉特征。1.3 系统整体架构概览整个系统的架构并不复杂核心是三层存储层MySQL存业务元数据和分类标签Qdrant或类似向量数据库存图片特征向量OSS或本地磁盘存原图。模型层用PyTorch或TensorFlow训练图片分类模型导出成可以部署的服务再抽一个特征提取模型为每张图生成固定长度的embedding。应用层提供上传接口、分类接口、检索接口还有简单的管理后台。这套架构的好处是每一层都能独立扩展。比如图片量从几万涨到几百万可以把MySQL换成分布式版本把特征向量迁移到分布式的向量库模型服务多部署几个副本。对于中小电商团队来说先用单机版本跑通流程后面再逐步拆分成本完全可控。2. 数据库设计与资源准备2.1 图片元数据表结构设计数据库是这套系统的地基我先把MySQL表结构放出来。这里为了方便说明我简化了一些字段但核心结构基本就是这些。CREATE TABLE product_image ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, product_id varchar(64) NOT NULL COMMENT 商品ID, sku_id varchar(64) DEFAULT NULL COMMENT SKU ID, image_url varchar(512) NOT NULL COMMENT 图片访问路径, image_hash char(32) NOT NULL COMMENT 图片去重哈希md5, category_id int(11) DEFAULT NULL COMMENT 预测类目ID, category_name varchar(128) DEFAULT NULL COMMENT 预测类目名称, confidence decimal(5,4) DEFAULT NULL COMMENT 分类置信度, audit_status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待审核 1通过 2驳回, upload_source varchar(32) DEFAULT NULL COMMENT 图片来源渠道, extra_info json DEFAULT NULL COMMENT 扩展信息, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_product_id (product_id), KEY idx_category_id (category_id), KEY idx_image_hash (image_hash) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT电商产品图片元数据表;这里有几个字段我要特别解释一下。image_hash是用来做精确去重和查重的上传新图时先算MD5如果库里已经有了就直接跳过或者走“更新引用”的逻辑避免同一个图片文件被重复存储。category_id和category_name来自图片分类模型的输出confidence记录模型判断的置信度低置信度的图会进入人工审核队列不会直接发布这个设计对电商场景非常重要。表结构设计完成之后增删改查就是基本功了。我用Python写了个简单的数据访问层统一封装插入、查询、按类目统计等方法业务代码里不直接拼SQL后面维护起来省心很多。注意extra_info字段用JSON类型可以存一些模型版本、人工修正标签这类非结构化信息比不停加字段灵活。2.2 向量数据库选型与特征存储关系型数据库存储的是结构化标签但光有标签还远远不够。用户有时候会拿一张图来找“类似的商品”这种需求用关键词或SQL根本没法实现必须用向量检索。我给每个图片生成一个512维或1024维的特征向量存进向量数据库然后通过余弦相似度计算图片之间的相似度。目前我用的是Qdrant纯开源的向量数据库部署和API都很轻量。为什么选Qdrant而不是其他方案第一它支持Docker一键启动对于小团队来说非常友好第二Python SDK封装得比较完善和FastAPI、PyTorch生态配合得很好第三它支持payload过滤也就是说可以先用业务字段过滤比如“只看女装类目”再在过滤结果里做向量相似度检索这一点非常实用。创建collection的代码大致是这样from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams client QdrantClient(hostlocalhost, port6333) client.recreate_collection( collection_nameproduct_image_vectors, vectors_configVectorParams(size512, distanceDistance.COSINE), )这里的size要跟模型输出的embedding维度保持一致。我们用ResNet50倒数第二层输出2048维为了节省存储和加速检索会再加一个全连接层降到512维。向量数据入库之后查询时直接带上图片特征向量Qdrant会返回相似图片的ID和分数再回到MySQL里查图片的详细信息一套完整的以图搜图链路就通了。2.3 数据同步与迁移工具选型真正上线之后你会发现光有元数据表还不够数据同步是个大话题。电商系统通常有多个环境测试环境、预发环境、生产环境还可能有不同机房之间的备份。我们最初是手动导出SQL再导入搞了几次就受不了尤其是图片元数据这种增长很快的表全量导出导入不仅慢还容易锁表。后面我调研了开源的异构数据库同步工具最后选定用DBSyncer来做MySQL之间的准实时同步它支持监听binlog把增量变更自动同步到目标库。对于图片数据这种强一致性要求不高的业务准实时同步已经够用了。如果同步过程中出现类型不兼容的问题DBSyncer里可以配置字段映射关系。另外向量数据库的特征数据我暂时没做双活只是定期用快照备份原因是量级还没到必须实时同步的地步等图片量破百万再考虑扩展。这里要提醒一句任何数据库同步工具都不能完全取代定期的全量备份。我每周日凌晨会跑一次全量备份保留最近四周的快照。之前有一次同步任务挂了半天没发现全靠备份才把数据恢复到故障前状态。3. 图片分类模型训练与实现3.1 数据集的采集与标注做分类模型之前最大的工作量其实在数据准备。我的数据集主要来自两部分一部分是公司历史上已经人工分好类的商品图片另一部分是从公开的电商图片素材库整理来的。因为电商图片涉及版权和隐私问题我不建议随意爬取他人店铺图片更不要拿用户图片做训练合规风险太大。更好的方式是先把自有图片盘点清楚再补充公开数据集。这里说一下四分类花卉模型的小例子。我最早为了验证模型流程用玫瑰、向日葵、郁金香、菊花四个类别各500张图总共2000张做了个四分类实验。训练流程跑通后我把模型结构换成电商类目从历史数据里抽了“女装”“男装”“鞋靴”“箱包”四类图片同样能快速收敛。关键点是每个类别的图片数量尽量均衡背景和拍摄角度要有变化否则模型见过太多白底图遇到实拍图就会误判。标注环节我一开始用人工一张张看效率太低。后来发现可以先让未训练的模型给一批图打上伪标签人只需要修正错误的部分整体标注效率能提升一倍以上。我强烈建议在标注工具里增加快捷分类键比如按数字键1-4对应四个类目这样一小时每人能标几百张图。3.2 基于迁移学习的模型训练流程我用的框架是PyTorch模型基于torchvision里的ResNet18。迁移学习的思路很简单把预训练模型最后一层全连接替换成我们自己的类别数然后分阶段训练——先冻结所有卷积层只训练新加的全连接层收敛后再解冻部分后层用小学习率微调。这样做的理由是预训练模型已经学会了通用图像特征前期的卷积层对大多数任务都有效没必要一开始就全部更新否则数据量不够很容易过拟合。核心训练代码我简化一下import torch import torch.nn as nn from torchvision import models, transforms from torch.utils.data import DataLoader model models.resnet18(pretrainedTrue) num_classes 4 model.fc nn.Linear(model.fc.in_features, num_classes) # 阶段一冻结卷积层只训练全连接层 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) # 训练循环省略这里只展示核心流程 for epoch in range(epochs): for inputs, labels in train_loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step()训练循环本身不复杂真正的坑在数据预处理。图片分类模型的输入通常是224x224的RGB图要让模型效果好transform里要加随机裁剪、水平翻转、颜色抖动这些数据增强手段。做电商图片分类时我发现还要保留一定的宽高比不能粗暴地拉伸变形否则模型会把形变学进去导致真实场景下准确率下降。所以我的transform里用了Resize((256, 256))再加CenterCrop(224)既保证尺寸统一又减少形变影响。3.3 把模型部署成可用的分类服务模型训练好之后不能只停留在notebook里要变成业务可调的接口。我用FastAPI封装了一个极简的分类服务流程是接收图片URL或二进制流先做预处理然后跑模型推理返回类目标签和置信度。FastAPI自带API文档调起来非常方便。from fastapi import FastAPI, UploadFile from PIL import Image import torch import torchvision.transforms as transforms app FastAPI() model load_model() # 加载训练好的模型 app.post(/classify) async def classify_image(file: UploadFile): image Image.open(file.file).convert(RGB) image transform(image).unsqueeze(0) with torch.no_grad(): outputs model(image) probs torch.softmax(outputs, dim1) conf, pred torch.max(probs, dim1) return {category_id: pred.item(), confidence: round(conf.item(), 4)}部署之后整个图片上传链路就变成了上传图片到OSS返回URL然后把URL发给分类服务拿到类目标签再连同原图信息一起写入MySQL。分类服务我做成了独立进程跟业务服务分开部署这样模型升级不影响主业务也方便单独做GPU卡资源分配。4. 实操过程与核心环节实现4.1 从数据库创建到导入图片元数据实操的第一步是把数据库和表结构先建起来。我在MySQL里执行上面那段建表SQL然后写了一个Python脚本遍历指定目录下的商品图片计算MD5调用分类服务最终把图片路径、哈希、预测类目等信息做成批量插入一次性写入MySQL。import hashlib import requests import mysql.connector from pathlib import Path def md5_file(path): h hashlib.md5() with open(path, rb) as f: for chunk in iter(lambda: f.read(4096), b): h.update(chunk) return h.hexdigest() conn mysql.connector.connect(hostlocalhost, userroot, password****, databaseecom_image) cursor conn.cursor() image_dir Path(/data/product_images) for img_path in image_dir.glob(*.jpg): image_hash md5_file(img_path) # 调用分类服务 with open(img_path, rb) as f: resp requests.post(http://localhost:8000/classify, files{file: f}) result resp.json() sql INSERT INTO product_image (product_id, image_url, image_hash, category_id, category_name, confidence) VALUES (%s, %s, %s, %s, %s, %s) vals (img_path.stem, f/images/{img_path.name}, image_hash, result[category_id], result[category_name], result[confidence]) cursor.execute(sql, vals) conn.commit() cursor.close() conn.close()这里有个很重要的细节批量导入一定要用INSERT一次插入多行而不是一条条提交。我最初用的是循环里逐条commit导入一万张图花了快半小时改成批量提交后同样数据只要几十秒性能差距非常大。另外product_id如果暂时没有可以用图片文件名或者SKU编码代替等后续业务数据关联上再更新。4.2 图片去重与特征向量生成流水线导入图片元数据的同时还需要为每张图生成特征向量并写入向量数据库。这里我把“去重”和“特征提取”放在同一条流水线里逻辑是先算MD5判断是否已经存在存在就直接跳过不存在才做特征提取和入库。但MD5只能判断完全相同的图片对于改过尺寸、调过滤镜的“近似重复图”需要用到感知哈希或者计算特征向量的相似度。我使用的流程是先用感知哈希做一次初筛把相似度比较高的图片聚在一起再通过向量数据库计算精确的余弦相似度最后人工确认是否确认为重复。这样兼顾了速度和准确率。特征向量提取的代码跟模型训练复用同一个模型只是去掉最后的分类层取倒数第二层输出作为embedding。我试过直接存2048维向量检索速度也能接受但为了节省内存后面统一降到了512维。from qdrant_client import QdrantClient from qdrant_client.models import PointStruct qdrant QdrantClient(hostlocalhost, port6333) embedding extract_embedding(img_tensor) # shape: [512] qdrant.upsert( collection_nameproduct_image_vectors, points[PointStruct(idimage_id, vectorembedding.tolist(), payload{category_id: category_id})] )写向量库的时候建议把category_id一起放进payload这样后面做“在某个类目内找相似图”就非常快。如果只存向量而不存业务属性检索时还需要回到MySQL过滤一遍多一次网络开销排序也可能不准确。4.3 检索与分类接口联动图片分类和向量检索都跑通之后我把它们整合成了业务接口。比如运营想“找出所有跟这张图相似的女装连衣裙图片”先调用分类服务拿到这张图的类目和向量然后去向量数据库里做相似度检索再用返回的图片ID去MySQL查详情。核心查询代码import requests from qdrant_client import QdrantClient import mysql.connector # 1. 获取查询图的分类和embedding query_resp requests.post(http://localhost:8000/classify, files{file: query_img}).json() query_vector get_embedding(query_img) # 2. 从向量库检索相似图 qdrant QdrantClient(hostlocalhost, port6333) hits qdrant.search( collection_nameproduct_image_vectors, query_vectorquery_vector, query_filtermodels.Filter(must[models.FieldCondition(keycategory_id, matchmodels.MatchValue(valuequery_resp[category_id]))]), limit20, ) # 3. 从MySQL查详情 image_ids [hit.id for hit in hits] conn mysql.connector.connect(...) cursor conn.cursor() cursor.execute(SELECT id, product_id, image_url, category_name, confidence FROM product_image WHERE id IN (%s) % ,.join(map(str, image_ids))) rows cursor.fetchall()这里要注意检索返回的hit.id一定要跟MySQL表里的id保持一致否则关联不上。我最初踩过这个坑向量库里的自增ID和MySQL的主键是两套体系查出来的图对不上后来统一用MySQL主键作为向量库的ID才算解决。类似这种“双写一致性”问题在做跨系统集成时几乎必然遇到设计初期就要明确好ID映射关系。5. 常见问题与排查技巧实录5.1 数据库常见问题速查表做这套系统的过程中我在数据库上踩的坑最多很多都是很常见的经典问题。我整理了一个速查表都是实际遇到并且验证过的解决方案。问题现象可能原因解决办法MySQL报死锁错误多个事务同时更新同一批图片元数据加锁顺序不一致尽量按照相同顺序更新记录开启innodb_deadlock_detect重试机制捕获死锁异常自动重试从Oracle导出的身份证号显示成科学计数法导出到Excel时数字列被转成数值类型超过15位精度丢失导出时把身份证字段强制设为文本格式或SQL中用TO_CHAR转为字符串再导出MySQL连接池满导致接口超时业务高峰期连接数不够或者连接未及时释放调整连接池最大连接数代码里用with确保连接关闭排查慢查询向量检索结果和业务筛选不一致Qdrant的payload过滤条件写错或ID映射不一致先单独测试filter条件确认返回数量再和MySQL结果比对数据库同步任务延迟加大binlog解析慢或目标库IO瓶颈调整同步任务的批量大小监控目标库主键索引必要时升级同步工具所在机器配置死锁问题我多说一句电商图片表经常会被多个后台任务同时读写比如“批量打标任务”和“审核状态更新任务”同时跑就容易出现事务交叉。解决思路是尽量让每个任务都走同一个索引顺序比如都先按product_id分组处理而不是有的按category_id、有的按id这样能大幅降低死锁概率。5.2 图片分类模型准确率上不去的排查思路很多朋友遇到模型准确率低就急着换模型、调学习率但我实际经验表明90%的情况问题出在数据上而不是模型上。我排查的顺序是先看各类别样本量是否均衡再看标注是否准确最后才看训练参数。训练四分类花卉模型时第一次准确率只有82%我一看混淆矩阵发现郁金香和玫瑰经常互相混。原因是两类花的颜色和形状在某些拍摄角度下确实很像而且训练集里郁金香的图片大多是侧面照玫瑰大多是俯拍模型学到了拍摄角度而不是花本身。解决方案是增加数据增强加入随机旋转、随机透视变换同时补充更多不同角度的郁金香图片。调整之后准确率提升到了93%。电商类目的情况更复杂。有一阵子模型老是分不清“皮鞋”和“运动鞋”后来发现是因为白底图侧拍的样本占绝对多数而用户实拍图大部分是俯拍导致真实场景泛化差。我的经验是训练集里要刻意保留一部分“脏数据”模拟线上真实场景而不是只挑干净漂亮的白底图。5.3 性能优化从千级到十万级图片量的实践最开始系统只有几千张图片任何操作都是秒回。等图片量涨到十万级问题就冒出来了批量导入变慢向量检索偶尔超时后台统计接口要好几秒。我做了几轮优化效果最明显的有三个。第一是MySQL索引优化。之前对category_id和created_at分别建了单列索引但统计“某类目近30天图片数”时还是慢。后来改成联合索引(category_id, created_at)查询效率提升了近十倍。第二是向量检索增加阈值控制。很多相似度很低的垃圾结果其实业务上不需要我加了score_threshold或者直接取TopK把无效计算挡在查询之外。第三是引入了异步任务队列图片上传后就返回“处理中”实际的哈希计算、分类推理、向量入库都丢到Celery里异步执行接口响应时间从2秒降到了200毫秒以内。另外如果图片量继续增长到百万级、千万级MySQL单表可能扛不住建议水平分表按product_id做hash分片。向量库也可以从单机Qdrant升级成集群模式。这个扩展路径我们已经在规划中核心原则是存储和计算拆开前端接口不受后端扩展影响。最后再分享一个小技巧做图片分类数据库这件事一定要先把“标签体系”定义清楚再动手。我一开始直接沿用业务里的类目ID后来发现类目树经常调整有的图片甚至同时属于多个类目导致分类模型训练和SQL查询都很痛苦。后来改成了独立于业务类目的“视觉分类”标签只反映图片上能看出来的内容跟业务类目做映射关系灵活性高了很多。如果你也在做类似的系统我建议你从一开始就把握好这个边界后面会少走很多弯路。
返回列表