当前位置: 首页 > news >正文

深度哈希实战:端到端训练实现毫秒级相似性搜索

1. 这不是“又一篇综述”,而是一份能跑通、能调参、能落地的深度哈希实操手记

我从2018年开始在电商搜索推荐系统里做相似性检索优化,当时团队正被千万级商品图库的实时召回压得喘不过气——用传统CNN提取特征再算余弦相似度,单次查询平均耗时320ms,QPS卡在47,根本扛不住大促流量。后来我们把整个图像相似搜索链路重构为端到端深度哈希方案,上线后QPS飙升到3100+,平均响应压到11ms以内,存储开销从TB级降到GB级。今天这篇不是照搬论文的“文献综述”,而是我把三年来在生产环境反复打磨、踩坑、调优的经验,掰开揉碎了写给你看。核心关键词就三个:深度哈希(Deep Hashing)相似性搜索(Similarity Search)端到端训练(End-to-End Training)。它解决的是一个非常具体的问题:当你面对海量高维数据(比如千万张商品图、亿级用户行为向量),如何在毫秒级内找到最相似的Top-K个样本,同时把存储和计算成本压到最低?适合三类人直接抄作业:正在做图像/视频/文本检索的算法工程师、需要快速搭建轻量级相似搜索服务的后端开发者、以及想真正搞懂“为什么哈希码能代替原始特征”的研究生。下面所有内容,都来自真实线上系统的日志、监控截图和调试记录,没有一句空话。

2. 深度哈希的本质:不是“压缩”,而是“语义重编码”

2.1 为什么传统方法在这里会失效?

先说清楚一个常见误解:很多人以为深度哈希就是“用神经网络把大向量压成小向量”,这完全错了。如果你只是把512维ResNet特征强行接个全连接层降维到64维,再用sigmoid截断成0/1,结果会非常灾难——我在测试集上跑过,mAP(mean Average Precision)直接从0.82暴跌到0.31。问题出在哪?关键在于语义保真度丢失。传统降维(如PCA)或浅层哈希(如LSH)只关注数学距离,但深度哈希的核心目标是:让语义相似的样本在汉明空间里也彼此靠近。举个例子:两张不同角度拍摄的同一款iPhone手机图,在原始像素空间欧氏距离可能很大,但在语义空间它们应该高度相似。深度哈希要学的,正是这个从“像素→语义→二进制码”的非线性映射函数。

提示:汉明距离(Hamming Distance)就是两个等长二进制串对应位不同的个数。比如10101001的汉明距离是2。深度哈希的终极目标,就是让语义相似的样本对,其哈希码的汉明距离尽可能小(理想是0),不相似的则尽可能大(理想是码长)。

2.2 深度哈希与传统哈希的根本区别

维度传统哈希(如LSH)深度哈希(Deep Hashing)
哈希函数来源手工设计的随机投影或核函数(如SimHash)神经网络自动学习的非线性映射(CNN+FC+Sign)
输入依赖完全数据无关(Data-Independent)强烈依赖训练数据分布(Data-Dependent)
相似性定义基于原始空间的数学距离(L2, Cosine)基于监督信号定义的语义相似性(Pairwise/Label)
优化目标最小化哈希碰撞概率最小化语义相似性与汉明距离的错配损失
实际效果高维稀疏数据下召回率骤降在ImageNet、CIFAR等标准数据集上mAP超0.9

这个表格背后是本质差异:LSH假设数据服从某种理想分布(如均匀球面),但真实世界的数据(比如电商图)极度不均衡——80%的图片集中在“服装”“手机”“美妆”几个类目,且同类目内存在大量细粒度差异(同款T恤不同模特、不同光照)。深度哈希通过端到端训练,让网络自己发现这些隐含的语义结构,并将其编码进二进制码中。我在线上系统里对比过:用LSH处理100万张服装图,Top-10召回准确率仅63.2%;换成DPSH后,同一硬件配置下准确率升至89.7%,且索引构建时间缩短40%。

2.3 为什么必须是“端到端”?——特征学习与哈希学习的耦合陷阱

很多初学者会尝试“两阶段训练”:先用ImageNet预训练好的ResNet提取特征,再单独训练一个哈希网络把特征映射成二进制码。这看似合理,实则埋下巨大隐患。我在2020年双11前夜就栽在这上面:两阶段方案在离线测试mAP达0.85,但上线后首小时就出现大量误召回——搜“黑色连衣裙”结果里混入大量“黑色西装裤”。排查发现,预训练ResNet的特征空间与业务场景严重脱节:它在ImageNet上学到的“黑色”是泛化的色彩概念,而业务需要的是“服装领域中黑色连衣裙特有的纹理、剪裁、领型组合特征”。端到端训练强制网络在哈希约束下重新学习特征表示,相当于给特征提取器加了一个“语义滤镜”。DPSH论文里那个反馈机制(Feedback Mechanism)绝非噱头——它让哈希层的梯度反向穿透到CNN底层,迫使卷积核去捕捉那些对区分“连衣裙vs西装裤”真正关键的局部模式(比如袖口褶皱、腰线位置)。实测数据:端到端DPSH比两阶段方案在服装细粒度检索任务上mAP提升12.3个百分点。

3. 四大主流深度哈希方法深度拆解:原理、代码、避坑指南

3.1 DPSH(Deep Pairwise-Supervised Hashing):监督信号最直接的方案

Li等人2015年提出的DPSH,至今仍是工业界首选。它的核心思想极简:给定一对样本,如果标签是“相似”,就让它们的哈希码汉明距离小;如果“不相似”,就让距离大。但实现远比听起来复杂——直接用汉明距离求导是不可能的(Sign函数不可导),所以DPSH用了一个精巧的代理函数。

原理透析
DPSH的损失函数由两部分构成:

  1. Pairwise Classification Loss:将汉明距离转化为相似性得分S = (L - d_h) / L(L为码长,d_h为汉明距离),再用交叉熵衡量预测相似性与真实标签的一致性。
  2. Quantization Loss:惩罚连续输出与离散目标的偏差,用||h - sign(h)||²形式,其中h是网络输出的连续值(-1~1之间)。

关键洞察在于:Quantization Loss不是可有可无的正则项,而是保证二值化质量的生命线。我曾删掉这一项做对比实验,结果网络输出的h值集中在[-0.3, 0.3]窄带内,sign后大量码位变成随机0/1,mAP直接归零。

PyTorch核心代码片段(可直接复用)

import torch import torch.nn as nn class DPSHLoss(nn.Module): def __init__(self, code_length, alpha=1.0): super().__init__() self.code_length = code_length self.alpha = alpha # quantization loss权重 def forward(self, hash_code, labels): # hash_code: [batch_size, code_length], values in [-1, 1] # labels: [batch_size, batch_size], 1 for similar, -1 for dissimilar # 1. 计算汉明距离的代理:内积近似(因hash_code∈[-1,1],内积≈L-2*d_h) similarity_matrix = torch.matmul(hash_code, hash_code.t()) # [B, B] # 2. Pairwise Classification Loss (交叉熵) # 将内积映射到[0,1]作为相似性概率 prob_sim = (similarity_matrix + self.code_length) / (2 * self.code_length) # 交叉熵:-y*log(p) - (1-y)*log(1-p),此处y为labels映射后的0/1 label_binary = (labels > 0).float() cls_loss = -label_binary * torch.log(prob_sim + 1e-6) \ - (1 - label_binary) * torch.log(1 - prob_sim + 1e-6) cls_loss = cls_loss.mean() # 3. Quantization Loss quant_loss = torch.mean(torch.pow(hash_code - torch.sign(hash_code), 2)) return cls_loss + self.alpha * quant_loss # 使用示例 criterion = DPSHLoss(code_length=64, alpha=0.5) loss = criterion(hash_output, pairwise_labels)

实操心得

  • 码长选择:64位是黄金平衡点。32位时mAP下降明显(尤其在细粒度任务),128位存储翻倍但mAP增益不足2%。我们最终在电商场景固定用64位。
  • alpha调参:初始设0.5,若训练后期hash_code的绝对值均值<0.8,说明量化不足,需增大alpha;若>0.95且验证集mAP震荡,则减小alpha。
  • 致命陷阱:Pairwise labels矩阵内存爆炸!10000样本需100MB内存。解决方案:动态采样——每轮只构造batch内样本的pairwise关系(即batch内所有样本两两组合),配合足够大的batch_size(我们用256)。

3.2 Triplet-based Deep Hashing:用排序思维解决相似性难题

Wang等人2016年提出的Triplet方法,灵感来自人脸识别的FaceNet。它不直接判断“AB是否相似”,而是问:“A与P(正样本)的距离,是否比A与N(负样本)的距离更近?”这种三元组排序范式,在类别边界模糊的场景(如“运动鞋”vs“休闲鞋”)表现更鲁棒。

原理透析
Triplet损失函数为max(0, d(A,P) - d(A,N) + margin)。但直接用汉明距离会导致梯度消失(d_h只能取整数),因此Triplet Hashing采用连续距离代理:用网络输出的连续向量计算欧氏距离,再通过sign得到最终哈希码。关键创新在于将量化误差显式建模进损失——在原始Triplet Loss基础上,增加一项||h_A - sign(h_A)||² + ||h_P - sign(h_P)||² + ||h_N - sign(h_N)||²

为什么它比DPSH更适合某些场景?
在我们的美妆类目测试中,Triplet方案mAP比DPSH高1.8%。原因在于:美妆产品相似性高度依赖局部特征(如口红膏体纹理、眼影盘分格形状),而DPSH的全局相似性标签容易忽略这些细节。Triplet通过强制模型关注“最易混淆的负样本”,倒逼网络学习更精细的判别特征。例如,当锚点A是某款YSL方管口红,正样本P是同款不同色号,负样本N是外形相似的MAC子弹头——网络必须学会区分膏体表面的细微反光差异。

避坑指南

  • 难样本挖掘(Hard Negative Mining)是生命线:随机采样的N样本90%都是简单负例(如口红vs键盘),对训练毫无帮助。我们采用在线难样本挖掘:每轮计算batch内所有三元组的损失,只保留损失最大的50%参与反向传播。
  • Margin设置:初始用0.2,若训练损失长期>0.1,说明margin过大,逐步降至0.1;若损失迅速趋近0但验证集mAP停滞,说明margin过小,需增大。
  • 注意:Triplet方法对batch_size极其敏感。我们实测发现,batch_size<128时,难样本数量不足,mAP波动剧烈;256是最优解,兼顾显存与效果。

3.3 DHN(Deep Hashing Network):双损失协同优化的工程典范

Zhu等人2016年的DHN,是首个将语义相似性学习哈希码质量控制明确解耦并协同优化的框架。它不像DPSH那样把两者揉在一起,而是用两个独立损失函数分别约束:交叉熵损失确保语义正确,量化损失确保二值化精准。这种设计极大提升了工程可控性。

架构解析
DHN的Pipeline清晰分为四段:

  1. Subnetwork(CNN主干):我们弃用论文中的AlexNet,改用轻量级ResNet18(参数量减少60%,推理快2.3倍);
  2. Hashing Layer(全连接层):输出维度=码长,不加激活函数(让梯度自由流动);
  3. Pairwise Cross-Entropy Loss:针对语义相似对;
  4. Pairwise Quantization Loss:针对所有样本对,计算||h_i - h_j||²与理想汉明距离的差距。

为什么双损失比单损失更稳?
单损失(如DPSH)容易陷入局部最优:网络可能学会一种“偷懒策略”——让所有hash_code趋近于[1,1,...,1],此时任意两样本汉明距离都为0,交叉熵损失极小,但完全丧失判别力。DHN的量化损失强制每个码位独立学习,因为||h_i - h_j||²的梯度会精确反向到每个维度。我们在训练日志中观察到:DHN的hash_code各维度标准差稳定在0.45±0.03,而DPSH初期常出现某些维度标准差<0.1(即该位几乎恒为0)。

实操配置表

超参数推荐值调参逻辑我们的线上值
学习率1e-4CNN主干用1e-4,Hashing Layer用5e-4(需更快收敛)1e-4 / 5e-4
交叉熵损失权重1.0基准值,不建议调整1.0
量化损失权重0.1~1.0初始0.3,若验证集汉明距离分布偏移(期望均值32,实际<28),则增大0.5
Batch Size256显存允许下越大越好(提升pairwise统计稳定性)256

3.4 DSDN(Deep Supervised Discrete Hashing):直面离散优化的硬核方案

Li等人2017年的DSDN,是唯一真正在离散空间内优化的方案。它彻底抛弃“连续代理+Sign”的套路,直接在二进制约束下求解。虽然计算复杂,但效果惊艳——在CIFAR-100上mAP达0.921,比DPSH高2.7%。

原理革命
DSDN的核心是交替优化(Alternating Minimization)

  1. 固定哈希码B,优化网络参数W使f_W(x) ≈ B
  2. 固定W,在离散约束B ∈ {-1,1}^n下,直接求解最优B(用半正定规划SDP或贪心算法)。

这相当于让网络和哈希码“互相校准”:网络告诉哈希码“你该长什么样”,哈希码反过来告诉网络“你生成的特征必须能精确匹配我”。

为什么值得为它多花30%训练时间?
在我们的奢侈品手表类目,DSDN的误召回率比DHN低37%。原因在于:高端手表的相似性极度依赖微小特征(如表盘罗马数字字体、表带缝线密度),这些特征在连续空间易被平滑掉。DSDN的离散优化强制网络聚焦于决定性的二值化判别点。例如,网络学到“表盘直径>40mm且指针末端有菱形刻度” →bit_1=1,这个规则比任何连续距离都更可靠。

工程落地技巧

  • SDP求解太慢?用贪心替代:我们实现了一个O(n²)贪心算法——每次翻转一个bit,选择使总损失下降最多的那个,迭代10轮。实测效果与SDP相差<0.3% mAP,但单次更新从分钟级降至毫秒级。
  • 内存优化:DSDN需存储所有样本的哈希码矩阵B(百万级×64bit),我们用内存映射(mmap)技术将其加载到SSD,访问延迟仅增加0.2ms。
  • 冷启动问题:首次训练时用DHN预热10个epoch,再切到DSDN流程,收敛速度提升3倍。

4. 从论文到生产:端到端部署全流程与性能压测实录

4.1 数据准备:不是“有标签就行”,而是“标签质量决定上限”

深度哈希的效果天花板,80%取决于训练数据的质量。我们曾用同一套模型,在标注质量差的内部数据集上mAP仅0.68,换用专业众包平台标注后跃升至0.89。关键在三个细节:

1. Pairwise标签的置信度分级
不能简单标“相似/不相似”。我们要求标注员对每对样本打分(1-5分),1分=完全无关(如“苹果”vs“汽车”),5分=几乎相同(同款商品不同拍摄角度)。训练时,将分数映射为软标签:5分→标签=1.0,3分→标签=0.6,1分→标签=0.1。这比硬标签提升mAP 4.2%。

2. 负样本的“难度梯度”
随机抽负样本效果差。我们构建三级负样本池:

  • Level 1(简单):不同大类(手机vs服装);
  • Level 2(中等):同类目不同子类(iPhone vs Samsung);
  • Level 3(困难):同子类易混淆款(iPhone 13 Pro vs iPhone 14 Pro)。
    训练时按3:5:2比例采样,确保模型持续挑战认知边界。

3. 数据增强的哈希感知设计
普通增强(旋转、裁剪)会破坏语义一致性。我们定制了哈希感知增强(Hash-Aware Augmentation)

  • 对正样本对,应用同步增强(同一随机参数作用于两张图);
  • 对负样本对,应用异步增强(不同参数);
  • 新增语义擦除:随机遮盖图像中判别性区域(如服装的logo、手机的摄像头模组),强迫网络学习更鲁棒的特征。
    这项改进使模型在遮挡场景下的mAP提升9.7%。

4.2 模型训练:避坑清单与监控指标

致命陷阱与解决方案

  • 陷阱1:Hash Code Collapse(哈希坍缩)
    表现:所有样本的hash_code趋近相同(如全为[1,1,...,1])。
    根本原因:量化损失权重过小,或学习率过高导致梯度爆炸。
    解决:监控torch.std(hash_code, dim=0),若任一维度标准差<0.05,立即降低学习率50%并增大量化损失权重。

  • 陷阱2:汉明距离分布偏斜
    理想情况:64位码,汉明距离应近似正态分布,均值32。若均值<25,说明码位相关性过高(冗余);>38,说明判别力不足。
    解决:引入码位独立性正则项loss += λ * ||Cov(hash_code) - I||²,其中Cov为协方差矩阵,I为单位阵。λ=0.01时效果最佳。

  • 陷阱3:训练-推理不一致
    训练时用sign(h),推理时若直接用sign(h),因浮点精度问题可能导致同一输入两次结果不同。
    解决:推理时统一用h > 0(而非sign(h)),并在训练最后阶段用h > 0替换sign(h)微调1个epoch。

关键监控指标(TensorBoard必看)

指标正常范围异常预警
hash_std_mean0.40~0.50<0.35 或 >0.55
hamming_dist_mean31.5~32.5(64位)偏离>±1.5
quant_loss0.05~0.15>0.25(量化不足)或 <0.01(过拟合)
cls_loss0.1~0.3连续5个epoch >0.35(学习率过高)

4.3 索引构建与在线服务:毫秒级响应的工程密码

模型训好只是开始,真正的挑战在服务层。我们线上系统支撑日均2.3亿次相似搜索请求,P99延迟<15ms。

索引构建三步法

  1. 批量编码:用TensorRT加速的ONNX模型,单卡(V100)每秒编码12,000张图;
  2. 汉明空间分桶:将64位哈希码视为64维超立方体顶点,用LSH Forest结构组织——对每个bit位建立倒排索引,查询时按汉明距离升序合并结果;
  3. 缓存预热:对高频Query(如“iPhone 14”),提前计算其Top-1000相似码,存入Redis Hash结构,命中率超65%。

性能压测实录(AWS p3.2xlarge)

场景QPSP50延迟P99延迟内存占用
100万商品图(64位)28508.2ms12.7ms1.2GB
1000万商品图(64位)31209.1ms14.3ms12.5GB
混合负载(80%相似搜索+20%特征提取)241010.5ms16.8ms14.8GB

关键优化点

  • 内存布局优化:哈希码不存为字符串,而用uint64_t数组连续存储,CPU缓存命中率提升40%;
  • SIMD加速:用AVX2指令并行计算汉明距离,单次比较64位仅需1个CPU周期;
  • 冷热分离:最近7天高频商品码存内存,其余存SSD,访问延迟差异<0.3ms。

5. 常见问题与实战排查手册:那些论文不会写的血泪教训

5.1 “训练时mAP很高,但线上效果差”——数据漂移的隐形杀手

这是最高频问题。2021年我们上线新模型后,离线测试mAP 0.91,但线上首周相似搜索点击率下降12%。根因分析发现:训练数据来自3个月前的用户行为,而新上架的“露营装备”类目在训练集中占比<0.1%,但线上请求中占23%。模型对这类新类目完全失效。

解决方案

  • 在线增量学习:每小时用最新10万次搜索Query的点击日志,微调Hashing Layer(冻结CNN主干),学习率设为1e-5;
  • 漂移检测:监控线上Query的哈希码分布熵值,若7天内熵值下降>15%,触发告警并启动增量训练;
  • 混合索引:对新类目,用少量样本训练专用小模型,与主索引并行查询,结果融合(加权平均)。

5.2 “为什么我的汉明距离总是0或64?”——量化损失失效的诊断树

hamming_dist_mean接近0或64时,说明量化环节崩溃。按此顺序排查:

  1. 检查hash_code输出范围
    torch.min(hash_code) > 0.5torch.max(hash_code) < 0.8,说明网络输出被“卡死”在正区间——检查CNN最后一层是否有意外的ReLU(必须去掉!);

  2. 检查量化损失计算
    确认是否用了||h - sign(h)||²而非||h - 0.5||²(后者是常见笔误);

  3. 检查学习率
    Hashing Layer学习率若>1e-3,极易导致梯度爆炸,hash_code迅速饱和;

  4. 终极方案
    在训练初期(前5个epoch)强制加入梯度裁剪torch.nn.utils.clip_grad_norm_(hash_layer.parameters(), max_norm=1.0)

5.3 “GPU显存爆了!”——大规模Pairwise计算的内存优化术

计算10万样本的Pairwise矩阵需40GB显存(10000x10000x4bytes)。我们用三种技术解决:

技术1:分块计算(Block-wise)
将样本分成1000个batch,每次计算一个batch与所有样本的相似性,用torch.cat拼接结果。显存峰值降至2.1GB。

技术2:FP16混合精度
在计算相似性矩阵时启用torch.cuda.amp,显存减少50%,速度提升1.8倍,mAP无损。

技术3:CPU卸载
对超大规模(>100万样本),将哈希码hash_code拷贝到CPU,用NumPy的cdist计算汉明距离(metric='hamming'),再传回GPU。虽慢30%,但显存占用为0。

5.4 “如何评估深度哈希是否真的work?”——超越mAP的实用指标

mAP是学术标准,但业务需要更直接的指标:

指标计算方式业务意义我们的达标线
Recall@10Top-10结果中相关样本占比用户是否第一眼看到想要的≥85%
Diversity ScoreTop-10结果的类目分布熵值避免结果同质化(如全是同款手机)≥1.2(5类目)
Click-Through Rate (CTR)相似搜索结果页的点击率直接反映用户体验≥18.5%
Fallback Rate触发“未找到相似结果”兜底逻辑的比例系统鲁棒性<2.3%

我们曾发现某版本mAP提升0.02,但Diversity Score从1.42暴跌至0.89——结果页全是同一款手机的不同角度图。立刻回滚,并在损失函数中加入类目多样性正则项:对Top-K结果,惩罚同类目样本过多。

6. 我的个人体会:深度哈希不是终点,而是新起点

在电商搜索系统里跑了三年深度哈希,我越来越确信:它不是一个“银弹”,而是一把需要不断打磨的瑞士军刀。最初我们迷信mAP,后来发现线上CTR才是真理;曾经追求极致精度,现在更看重服务稳定性——一次P99延迟超过20ms,用户流失率就跳升7%。最近半年,我们正把深度哈希和图神经网络结合:用哈希码作为节点ID,构建商品相似图,再用GNN做跨类目推理(搜“咖啡机”也能推荐“咖啡豆”)。这不是为了发论文,而是因为用户真实的搜索行为,从来就不在非黑即白的“相似/不相似”框里。最后分享一个小技巧:每次上线新模型前,我都会用100个典型Query手动检查Top-3结果——机器指标再漂亮,也比不上亲眼看到“用户想要的,真的排在第一位”那一刻的踏实感。

http://www.gsyq.cn/news/1552292.html

相关文章:

  • CRISP-ML(Q):面向落地的机器学习工程化标准流程
  • ML生产化不是部署模型,而是构建可信决策系统
  • Harness Engineering:从“使用AI“到“驾驭AI“的范式跃迁
  • 小模型接管前沿模型的四类确定性场景与工程落地方法
  • Word2Vec Skip-Gram 模型
  • AI代码评审落地失败的三大结构性断点与工程解法
  • 高校AI落地四层防御体系:从业务信任到决策闭环
  • 自主飞行系统实战解析:从模块化架构到适航落地
  • AI驱动数字孪生的实时闭环:从建模到产线落地的7个关键步骤
  • 多维聚合不是终点:让聚合结果可再操作的数据变形术
  • 2026年好用的网层板加工厂,金帆丝网口碑出众 - mypinpai
  • 低功耗高精度ADC选型:Σ-Δ架构原理与TC3402实战应用
  • DeepSeek-R1模型深度解析:推理增强原理与本地部署实践
  • 咳嗽声AI诊断:医疗音频分类的工程落地实践
  • YOLO26工业级对象裁剪:精准坐标映射与产线落地实践
  • C++SFINAE与enable_if应用
  • 在 Python 中,字符串切片使用语法 `s[start:stop:step]
  • 大模型深度思考能力实战评测:5个真实场景压力测试
  • 一站式跨平台影音管家:zyfun如何用技术重新定义桌面播放体验
  • 深度学习图像相似度实战:从特征嵌入到线上服务
  • 影刀RPA初学者必读:5个最常见误区与正确做法
  • Stable Diffusion生产级项目落地:从WebUI到可交付服务架构
  • AI可信四支柱:透明性、可追责性、隐私保护与无偏见性工程实践
  • Rnote:开源矢量手写笔记应用的终极指南
  • 口碑好的烘焙培训中心综合实力推荐 - myqiye
  • 豆包AI视频总结:重构视频信息处理工作流
  • 2026年南昌市PMP培训机构哪家好?官方授权R.E.P.报考指南 - 众智商学院课程中心
  • 聚焦AI时代反网络钓鱼,筑牢跨境通信安全防线——“一带一路”国家网络安全人才技能培训班成功举办
  • 专业的openclaw哪家更好
  • 漏洞修复实战指南:热修复与根治性修复的核心策略与工程实践