简介:这是一个基于YOLOv5的AI斗地主项目,面向对目标检测、深度学习与游戏决策感兴趣的开发者,旨在通过实时识别牌面图像并结合强化学习进行自动出牌。压缩包共39个文件,大小约14.42MB,以Python核心算法脚本为主,涵盖infer推理、数据集构建、模型评估等模块,同时包含C++/Java源码、Android工程配置(gradle、iml、xml)和预训练权重best.pt,便于从桌面端到移动端的多场景部署与试验。目前已有132人学习下载。包内不仅提供了可直接运行的检测模型和前后处理工具,还集成了ncnn-android-yolox示例,适合手机端轻量化改造;配合fast_dou_zero相关文件和多种辅助脚本,可进一步研究快速权重零化等强化学习加速技巧,探索斗地主的智能化策略。整体代码结构清晰、函数复用度高,既是游戏AI方向入门的实战参考,也可作为二次开发与算法改动的实验基底。
1. 先看清「基于 yolov5 的 ai 斗地主」到底是个什么东西
拿到这个标题的压缩包,第一反应别指望它是个能打赢高手的完整斗地主 AI。「yolov5」是目标检测模型,本职工作是“在一张图里框出物体并分类”,斗地主 AI 则是“根据手牌和出牌历史做决策”的博弈系统,两者本来不是一个赛道。这个项目能成立,靠的是一条很务实的工程链路:用 yolov5 识别牌桌上的牌面(包括手牌区、出牌区、底牌),把识别结果转成结构化手牌数据,再交给后端的出牌策略模块做决策。换句话说,yolov5 在这里扮演的是斗地主 AI 的“眼睛”,不是“大脑”。
真正把这个项目从玩具推到可用状态,坑全在后半程:环境版本不齐、牌面数据标注不准、推理速度跟不上、出牌逻辑和模型输出对不上。这篇就把整条链路拆开讲,从环境部署、数据构造、模型训练,到策略层的衔接和最后怎么验证这套 AI 到底会不会打牌。适合已经跑过 YOLO 目标检测、想把它迁移到非视觉场景的开发者,也适合想用斗地主做博弈 AI 入门、但不想从零写神经网络训练的那类人。读完你至少能跑通一个最小闭环,并且知道每个环节的边界在哪。
2. 跑通环境:yolov5 本地部署与依赖版本避坑
2.1 为什么选 yolov5 而不是直接上强化学习
斗地主 AI 的常规解法是强化学习,比如把牌局建模成马尔可夫决策过程,用 DQN 或类似算法去学出牌策略。但强化学习这条路对大多数个人开发者来说门槛太高:需要自建环境模拟器、设计奖励函数、处理对手策略的随机性,训练一次动辄几十个小时,而且结果很难调试。yolov5 介入之后,整个项目的难度曲线被压低了——你只需要解决“看清牌面”这一个视觉问题,出牌策略可以先用规则基的贪心算法顶着,后续再慢慢替换成更聪明的模型。
我一般会建议第一次做这个项目的人,把 yolov5 当作一个纯视觉前端,后端先不要碰强化学习。好处有三点:第一,yolov5 的生态非常成熟,预训练权重、标注工具、导出脚本都是现成的,最快几小时就能跑通检测;第二,牌面识别是一个边界清晰的目标检测任务,类别固定(54 张牌加背景),不像自然场景那样需要海量数据;第三,把视觉和决策解耦之后,你可以单独调试每一层,出牌逻辑写得再烂,也不会反过来影响检测模型的稳定性。等整个链路跑通了,再回头把策略层升级成学习型模型,心态会完全不一样。
2.2 最小可运行环境:Python 版本与依赖清单实测
yolov5 官方仓库对环境的限制其实很宽松,但实战中翻车最多的恰恰是版本组合。我推荐一套经过反复验证的最小组合:Python 3.9 或 3.10,PyTorch 1.13 或 2.0,CUDA 11.7 或 12.1,yolov5 用 6.2 或 7.0 分支。如果你是新机器,直接装 CPU 版 torch 也能跑推理,只是训练会慢到让人怀疑人生,所以有条件还是先把 CUDA 环境搞定。
# 创建独立虚拟环境,避免和系统 Python 打架 conda create -n yolo_doudizhu python=3.10 -y conda activate yolo_doudizhu # 安装 PyTorch,注意 cuda 版本要和驱动匹配 # 先执行 nvidia-smi 查看驱动支持的 CUDA 版本,再选对应的 wheels pip install torch==2.0.0 torchvision==0.15.0 --index-url https://download.pytorch.org/whl/cu118 # 克隆 yolov5 仓库并安装依赖 git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt这段命令里有几个关键点。conda create是为了隔离环境,不要图省事往 base 环境里装,后面装 opencv、onnx 这些包时版本冲突会非常头疼。torch的安装源用了 PyTorch 官方 whl 地址,这是因为 PyPI 上的 torch 默认带 CUDA 12.1,如果你机器只支持 CUDA 11.7,跑起来会直接报driver version is insufficient。先跑nvidia-smi看驱动支持的最高 CUDA 版本,再选对应的cu118或cu121后缀,这一步能省下后面一整天的排错时间。
依赖装完后,先用官方权重做一次推理验证环境。这一步非常关键,能确认 torch 和 CUDA 真的能协同工作,而不是装完就以为万事大吉。
# 用官方预训练权重测试一张自然图片,确认推理链路通畅 python detect.py --weights yolov5s.pt --img 640 --conf 0.25 --source data/images/bus.jpg如果命令行能正常输出检测结果并在runs/detect/exp下生成标注图片,说明环境没问题。如果卡在CUDA out of memory,先把 batch size 调到 1;如果报No module named 'torchvision',回到上一步检查requirements.txt是否完整安装。这套最小环境跑通之后,后续的模型训练、导出、部署都是在同一套环境里做,少换版本少踩坑。
2.3 首次推理验证:用一张牌桌图跑出牌面检测框
环境没问题后,先别急着训练,找一张真实的牌桌图片跑一次推理,感受一下 yolov5 在牌面场景下的表现。这里有个小技巧:直接用你手机拍的牌局照片,比网上随便下的示意图更接近真实使用场景,后续训练数据的分布也对得上。
# 用官方权重跑牌桌图,先看基线效果 python detect.py --weights yolov5s.pt --img 640 --conf 0.25 --source ./test_cards.jpg # 如果官方权重对牌面完全不感冒,考虑先用大图分辨率 python detect.py --weights yolov5s.pt --img 1280 --conf 0.2 --source ./test_cards.jpg第一次跑牌桌图,官方权重大概率什么都框不出来,或者把牌面误检成别的东西。这是正常的,因为 COCO 数据集里没有扑克牌这个类别。--img 1280这个参数值得说明一下:yolov5 推理时会把输入图缩放到指定尺寸,牌面文字在 640 下可能已经小到无法辨认,放大到 1280 能明显提升小目标召回率,代价是推理时间变长。对你后续训练来说,--img的大小也会影响标注精度,所以提前想清楚最终部署时用的是摄像头实时画面还是手机截图,这决定了你该用 640 还是 1280 作为基准分辨率。这一步跑通的意义在于:你确认了整条工具链可用,接下来才值得投入时间去标注牌面数据。
3. 构造牌桌数据集:从截图到 YOLO 格式的完整流程
3.1 牌桌数据长什么样:目标类别与标注规则
牌面检测的目标类别怎么定,直接决定模型训练难度和后续策略层的对接复杂度。最简单粗暴的方案是 54 个类别(52 张牌加大小王),但实战中我建议拆成「牌面数字」和「花色」两个维度分开处理,而不是直接设 54 类。原因是:yolov5 对类别数量的惩罚不大,但数据标注量会成倍增加——每张牌要标注出“红桃 A”和“黑桃 A”是两个不同类别,样本不均衡会非常严重。常见做法是让模型只检测牌面数值(A、2~10、J、Q、K、小王、大王共 15 类),花色信息通过后续的图像处理从固定位置抠出来识别。
标注规则上,有几个细节直接影响模型效果。第一,边框要尽量紧贴牌面印刷区域,不要留太多白边,否则不同牌之间边框大小差异大,模型学到的尺度特征不稳定。第二,每张牌的标注框要完整覆盖牌面数字,如果牌面有遮挡,宁可漏标也不要半标——yolov5 对标注噪声很敏感,半标框会让模型学到错误的边界。第三,底牌、手牌区、出牌区的牌要分开标注吗?我的建议是不要,让模型只负责找“牌在哪”,区域划分交给后处理逻辑,这样可以减少类别定义上的混乱。
# YOLO 格式标注示例:class_id, x_center, y_center, width, height # 所有坐标都是相对于图片宽高的归一化值 0 0.4532 0.6215 0.0834 0.1168 12 0.5138 0.6172 0.0819 0.1154 13 0.5821 0.6135 0.0824 0.1149这种标注格式是 yolov5 训练直接读取的,class_id对应类别清单里的索引,后面四个值是归一化坐标。标注工具我常用 labelImg 或 X-AnyLabeling,前者轻量、适合纯矩形框,后者支持半自动辅助标注,对大量牌面数据能省不少时间。标注时把可疑的模糊牌单独放一个「ignore」类别,训练时通过--noplots或数据清洗把它过滤掉,比硬着头皮乱标强。
3.2 用半自动标注脚本批量生成训练数据
如果全靠手工标注几百张牌桌图,效率太低也容易疲劳出错。这里分享一个常见的半自动流程:先用少量标注(比如 50 张)训练一个初版模型,再用这个模型去批量预标注剩余图片,人工只需要修正错误框。这在行业里叫「训练-标注-再训练」的迭代闭环,对牌面这类规则目标特别有效。
# 第一步:用初版模型批量生成伪标注 python detect.py --weights runs/training/exp0/weights/best.pt \ --img 1280 --conf 0.3 --source ./raw_images/ \ --save-txt --save-conf --project ./auto_label_out # 第二步:把 detect 输出的 txt 标签转成 YOLO 训练格式 python tools/convert_auto_labels.py \ --det_out ./auto_label_out \ --image_dir ./raw_images \ --out_dir ./labels--save-txt是关键参数,它让 detect 模式把每个检测框的类别和坐标写入 txt 文件,正好就是 YOLO 标注格式。--conf 0.3设得比平时高一些,目的是让伪标注尽量精准,少给后续人工修正添负担。转换脚本做的事其实很简单:把 detect 输出的每个图片对应的 txt 文件从runs/detect目录收集起来,统一放到labels目录,并保证图片名和 txt 名一一对应。转换时最常踩的坑是 yolov5 输出的坐标是像素值,而 YOLO 训练格式要求归一化坐标,脚本里要做一步除以图片宽高的操作;另一个坑是 detect 可能把背景误检成牌,伪标注里必然混入噪声,人工修正时优先删这种明显的错误框。
批量预标注能把 500 张图的标注时间从两天压缩到半天,前提是初版模型不能太差。如果初版模型在牌面数据上精度惨不忍睹,建议先手工标 100 张高质量数据把基础打牢,再进入迭代闭环。
3.3 数据增强与样本均衡:解决大小王和 A、2 太少的问题
牌面数据天然有个问题:普通数字牌样本量巨大,而大小王、A、2 这类“大牌”出现频率低。如果不做处理,模型对这些稀有类别的召回率会明显偏低,偏偏斗地主里判断牌型强弱最依赖的就是这几张牌。解决思路分两层,第一是数据层面做增强,第二是损失函数层面调权重。
# 用 albumentations 做针对性增强,模拟真实牌桌的光照和角度变化 import albumentations as A from albumentations.pytorch import ToTensorV2 transform = A.Compose([ A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.8), A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=15, val_shift_limit=10, p=0.5), A.Rotate(limit=30, border_mode=0, p=0.7), # 模拟牌面倾斜 A.RandomSizedBBoxSafeCrop(width=640, height=640, erosion_rate=0.2, p=0.5), ], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels']))这段增强策略是专门针对牌桌场景调的。RandomBrightnessContrast应对真实拍摄时光线不一致的问题,牌桌上方灯光、手机闪光灯都会造成亮度突变;Rotate处理牌面摆放不正的情况,斗地主出牌时牌很少整整齐齐一条线,倾斜是常态;RandomSizedBBoxSafeCrop在裁剪的同时保证所有标注框不被裁掉,这对牌面局部放大训练很有帮助。增强参数不是越大越好,旋转超过 30 度会让牌面文字变形严重,模型反而学不到稳定特征。
除了离线增强,yolov5 训练时自带的 mosaic、mixup 增强也默认开启,它们对小目标和大类别不平衡都有一定缓解作用。如果训练后稀有类别仍然检测不到,再去改损失函数权重——yolov5 支持在数据配置文件里给每个类别单独设权重,把大王、小王、2 这些类别的损失权重调高 1.5~2 倍,能逼着模型多关注这些少数样本。但权重别调太高,否则模型会过度拟合这几个类别,反而把普通牌的检测搞崩。
4. 训练牌面检测模型:参数设置与收敛判断
4.1 训练命令与关键超参数:img、batch、epochs 怎么定
牌面检测模型的训练命令和标准 yolov5 训练没有本质区别,但有几个参数需要根据你的数据量和部署目标特别调整。首先是--img,我建议直接用 1280,不要从 640 开始——牌面数字是典型的小目标,在 640 分辨率下可能只占 20×20 像素,特征信息太少;1280 虽然训练慢一些,但对最终检测精度的提升非常明显。其次是--batch,它受显存限制,如果你用的是 8G 显存的卡,1280 分辨率下 batch 只能开到 4 左右;显存不够时优先降 batch,而不是降分辨率。
python train.py --data card.yaml --weights yolov5s.pt \ --img 1280 --batch 4 --epochs 100 \ --project runs/training --name card_v1 \ --patience 20 --cachecard.yaml是数据配置文件,里面指定训练集、验证集的图片路径和类别清单。--patience 20的意思是连续 20 轮验证集指标没有提升就提前停止,这能省掉大量无效训练时间;--cache把图片加载进内存,减少磁盘 IO 等待,对训练速度影响很大,前提是你的内存足够。如果你用的是自己的数据,--weights yolov5s.pt建议保留,用 COCO 预训练权重做初始化比随机初始化收敛快得多,这也是 yolov5 生态最大的优势之一。
训练轮数方面,牌面检测任务一般 100 轮足够收敛,再多就容易过拟合。判断依据是验证集 mAP 是否还在稳定上升,如果 50 轮之后 mAP 变化不超过 0.5%,直接停掉换方案,别死磕。
4.2 判断模型真的收敛:从 loss 曲线到混淆矩阵
训练跑完后不要急着看 mAP,先看runs/training/card_v1目录下的 loss 曲线图和混淆矩阵。这两个文件能告诉你模型是正常收敛了,还是已经过拟合,或者在某个类别上完全没学会。
loss 曲线要看三条:box_loss、cls_loss、obj_loss。正常情况下它们都应该是平滑下降趋势,训练末期小幅波动是正常的。如果 box_loss 降但 cls_loss 横盘不动,说明模型能找到牌的位置但分不清牌面数值,大概率是标注里类别混淆太严重;如果所有 loss 都在降但 mAP 反而跌了,这是过拟合的典型信号,需要加数据增强或减少训练轮数。
混淆矩阵是排查类别问题的利器。以 15 类牌面为例,你重点关注两件事:一是大王/小王是否互相混淆,这两类长得确实像,但混淆了会导致后续策略层完全误判牌型;二是 A、2、J、Q、K 是否频繁误检成数字牌,这在牌面设计上太常见了——印刷字体接近,加上旋转和光照影响,数字和字母的区分度会显著下降。如果在混淆矩阵里看到某两个类别互相误检超过 10%,建议回标注阶段检查是不是框位置标偏了,而不是盲目加训练轮数。
4.3 导出模型:从 pt 到推理部署用的格式
训练完成后,模型默认保存为.pt格式,适合继续训练和评估,但不适合直接部署。常见做法是导成 ONNX 或 TensorRT 格式,前者跨平台兼容性好,后者在 NVIDIA GPU 上推理速度最快。这两个格式的导出都在 yolov5 仓库里自带脚本,但参数需要按部署环境调一下。
# 导出 ONNX,opset 版本按部署端支持情况选 python export.py --weights runs/training/card_v1/weights/best.pt \ --img 1280 --batch 1 --include onnx --opset 12 # 导出 TensorRT,注意要先装好 tensorrt 并确认 GPU 型号 python export.py --weights runs/training/card_v1/weights/best.pt \ --img 1280 --batch 1 --include engine --device 0导出时最容易翻车的两个点:一是--img必须和训练时一致,否则模型输入尺寸不匹配导致推理结果怪异;二是 ONNX 的 opset 版本,部署端如果用的是老版本 ONNX Runtime,opset 12 可能是上限,再高会直接报不支持。TensorRT 导出对 GPU 型号敏感,换一张卡就要重新导出一次,没有通用性,所以在最终部署环境上再执行导出流程,不要在自己电脑上导完拿过去用。导出完成后用一套验证集图片分别跑.pt和.onnx的推理,对比检测框数量和置信度分布,偏差在合理范围内才能上线。
5. 从牌面检测到出牌决策:策略层与状态机衔接
5.1 把检测框转成手牌:坐标排序与花色识别
模型输出的是「牌面数值 + 边框坐标」的列表,要变成斗地主可用的手牌数据,还差两步:坐标排序和花色识别。坐标排序的意义在于还原牌的实际顺序——斗地主的牌型判断里,连对、顺子这类组合对顺序极其敏感,如果检测框乱序,策略层再强也白搭。
def sort_cards_by_position(detections, row_epsilon=0.15): """ 检测结果:[(class_id, x_center, y_center, w, h), ...] 先按 y 分行,再按 x 排序,还原手牌从左到右的顺序 """ rows = {} for det in detections: _, xc, yc, w, h = det assigned = False for row_y in rows.keys(): if abs(yc - row_y) < row_epsilon * h: # 同一行的容忍度 rows[row_y].append(det) assigned = True break if not assigned: rows[yc] = [det] # 每行内部按 x 排序,行按 y 排序 sorted_cards = [] for row_y in sorted(rows.keys()): row_cards = sorted(rows[row_y], key=lambda d: d[1]) sorted_cards.extend(row_cards) return sorted_cards这段排序逻辑里,row_epsilon是最关键的参数。它表示两张牌在垂直方向上相差多少像素以内算同一行,我经验值取 0.15 倍的牌高;如果手牌区有牌被挡住导致检测框偏小,同一行牌的高度差异会拉大,这时要把row_epsilon调大。排序结果决定后续手牌字符串的生成,排错了就是满盘皆输,所以建议在调试阶段把排序结果可视化出来——在图片上按顺序画上每张牌的序号,人工检查是否和真实手牌顺序一致。
花色识别有两种常见做法。第一种是在标注阶段就把花色并入类别,即 54 类模型直接输出“红桃 A”这样的完整结果,缺点是前面提过的样本不均衡问题。第二种是检测到牌面数值后,在检测框固定的相对位置(通常是右下角或左上角)再切一个小图,用简单的模板匹配或一个小分类网络识别花色。我更喜欢第二种,因为花色符号即使在倾斜和模糊下也相对稳定,且切换新牌桌设计时只需要重训小花色分类器,不影响主模型。
5.2 出牌策略的分层设计:规则基 + 模型打分
牌型识别和出牌决策是两层。模型负责输出手牌和牌型结构,策略层负责决定“出什么”。第一版最可靠的做法是分层:最底层是合法的牌型枚举(单牌、对子、三带、顺子、连对、飞机、炸弹、王炸),中间层是跟牌逻辑(比上家大还是不出),最顶层才是局面评估——这一步可以用简单的规则评分,也可以留接口以后换强化学习模型。
def choose_response(hand_cards, last_move, is_landlord): """ 根据上家出牌和当前手牌,返回要出的牌型或不出 """ if last_move is None: # 自由出牌阶段,优先出最小单牌或最小对子 return play_smallest_hand(hand_cards) legal_moves = enumerate_moves(hand_cards, last_move) if not legal_moves: return None # 要不起 # 规则评分:能走完手牌优先,其次出最小的合法牌型 sorted_moves = sorted(legal_moves, key=lambda m: ( not can_finish(hand_cards, m), # 能不能一把走完 move_strength(m), # 牌型强度低优先 )) return sorted_moves[0]这段伪代码体现的是第一版策略的常见逻辑:能走完就走完,不走完就出最小的。enumerate_moves是核心函数,它负责把手中所有牌组合成合法的牌型,并且要确保压过last_move。这里最容易翻车的坑是顺子枚举的顺序——先枚举长度最长的顺子还是先枚举最小的单牌,会让 AI 的出牌风格完全不同。我建议先按长度从长到短枚举顺子,因为斗地主里顺子是最难凑也最容易卡手的牌型,早点把长顺子出掉能减少后期被憋死的风险。
规则基策略的上限大概在“能正常打完一局、不出非法牌、偶尔能赢新手”,再往上就力不从心了。所以接口设计要为后续留好路:choose_response这个函数的输入输出保持稳定,将来换成 DQN 或类似算法时,只需要替换内部的决策逻辑,不需要动牌型识别层。
5.3 完整推理链路:摄像头 / 截图 → 手牌 → 出牌建议
把检测、排序、策略串起来之后,整个系统的使用方式就清晰了。最实用的落地点是做成一个实时辅助工具:手机或摄像头对着牌桌拍照,系统自动识别当前手牌和出牌区,然后给出出牌建议。这种形态对性能要求很高,单帧推理时间要在 100ms 以内才不至于让体验太差。
# 使用 TensorRT 引擎做实时推理,帧率能提升到接近实时 python realtime_assist.py \ --engine runs/training/card_v1/weights/best.engine \ --img 1280 --conf 0.4 --source 0--source 0表示读取默认摄像头,换成视频文件路径也能跑。--conf 0.4比训练验证时候要高,因为在真实牌桌上误检的代价更大——你不想 AI 把桌面纹理当成一张牌,然后给出错误出牌建议。实时推理链路里,最常见的延迟瓶颈不在模型,而在后处理:每次检测出 20 张牌后要排序、要识别花色、要更新手牌状态,这些 Python 层的计算如果没优化,可能比模型推理还慢。建议把排序和策略层改成每次只处理「新增检测结果」的增量模式,而不是每帧都全量重算;虽然第一版可以接受全量重算的慢速,但如果你打算做成实时工具,这一步优化早晚要做。
6. 避坑清单:环境、数据、推理三个环节的常见问题排查
6.1 环境坑:CUDA 版本与 torch 不匹配导致的 infer 卡死
现象:执行detect.py时模型加载正常,但一跑推理就卡住,终端没有报错,GPU 显存却被占满不动。
原因:最常见是 torch 的 CUDA 版本和驱动支持的版本不匹配。比如驱动只支持 CUDA 11.7,但你装了cu121的 torch,运行时 CUDA 上下文初始化失败却没有显式报错,而是卡在某个底层同步点上;另一种情况是 torch 和 torchvision 版本不匹配,造成算子图编译时死锁。
解决:先跑nvidia-smi看驱动版本和支持的 CUDA 版本,再确认 torch 的编译版本:python -c "import torch; print(torch.version.cuda)"。两者要对得上。如果驱动版本太老,优先降 torch 而不是升级驱动;如果是 torch 和 torchvision 不匹配,用官方pip install torch==xxx torchvision==yyy --index-url重装一套固定版本。再不行就卸载重装,别在坏环境上继续折腾。
6.2 数据坑:标注框太小或牌面倾斜导致的类别混淆
现象:训练完模型后,A 和 4、J 和 1 频繁互相误检,大王偶尔被识别成小王,验证集 mAP 卡在 0.8 上不去。
原因:回查标注数据会发现两类问题。一是标注框画得太紧,只框住了牌面数字的主体但漏掉了笔画边缘,导致模型学到的特征不完整;二是数据增强里的旋转角度设得太大,牌面倾斜超过 30 度后,A 和 4 这种外观接近的类别本来就不容易区分,增强反而放大了混淆。
解决:统一标注标准,框必须覆盖整个印刷数字区域,留出 2~3 像素的安全边距;把增强里的Rotate上限从 30 度降到 15 度,同时增加 5 度以内的小角度旋转样本;在训练集里手动补充 A 和 4、J 和 1 的对比样本——把容易混淆的牌放到同一张图里,让模型学会区分它们的边界特征。这个坑在牌面检测里几乎人人都会踩一次,早发现早解决,别等到训练完才回头看标注。
6.3 推理坑:模型预测置信度低、重复检测与漏检
现象:模型在验证集上 mAP 表现不错,但实际跑牌桌图时,一张牌被框出两三个重叠框,或者部分牌完全漏检,置信度普遍低于 0.3。
原因:验证集和你实际使用的图片分布不一致,这是最常见的根源。验证集如果是规整的截图,训练时学到的特征就偏向“干净牌面”;真实摄像头画面里可能有手指遮挡、阴影、反光,这些噪声让模型输出的置信度全面下降。重复检测则是因为 NMS 阈值设得太低,重叠框没有被抑制掉。
解决:针对分布不一致,把真实场景图片补充进训练集,用 3.2 节提到的半自动标注流程快速迭代;针对置信度低,先把--conf从 0.25 降到 0.15 观察检测效果,如果只是多了几个噪声框而牌都能找全,说明模型本身没问题,是阈值选得不合适;针对重复检测,把 NMS IoU 阈值从默认的 0.45 降到 0.3,重叠框会明显减少。这几个参数没有标准答案,要在你的数据上直接实验。
6.4 策略坑:牌型识别对但出牌顺序导致连牌算法死循环
现象:AI 持有 3~10 的顺子,但出牌逻辑在枚举组合时卡死,或者反复尝试出同一张牌然后又被自己收回,整局游戏无法推进。
原因:连牌枚举算法实现有缺陷。常见写法是从最小牌开始尝试构建顺子,但如果没有处理“当前牌已经被前面的组合占用”的状态,算法会在递归中反复选中同一张牌,形成死循环;另一种情况是枚举顺子时没判断牌面数量是否足够,3~10 需要 8 张牌,如果手里只有 7 张还硬枚举,就会进入死循环。
解决:枚举前先对手牌做去重和计数,把每张牌的数量统计成字典;枚举顺子时按“从长到短”的顺序,并且每选中一张牌就从可用池里移除,递归回溯时再放回。另外加一个最大尝试次数保护,比如枚举 1000 次还没找到合法组合就放弃该轮出牌,调用“要不起”逻辑跳过,避免单局卡死。这类逻辑 bug 单测很难完全覆盖,建议在策略层用「预置牌型」做单元测试,列出 10 组典型手牌组合,每次改出牌逻辑后都跑一遍回归。
7. 让 AI 斗地主从「能跑」到「打得像样」:验证与进阶
7.1 用回放数据离线评估决策质量
检测模型能用 mAP 评估,策略层怎么评估?一个务实的办法是收集真实牌局回放数据,把每一手的历史出牌记录喂给你的 AI 做“复盘”。具体做法是:从回放里取出某个时刻的手牌和上家出牌,让 AI 给出建议,再对比回放里真实玩家出的牌。不需要 AI 完全同意真实玩家,但你可以统计一个「合理性指标」——AI 的决策和真实玩家出牌属于同一牌型强度的比例。
def evaluate_strategy(hands_history, ai_decision_fn): hit = 0 total = 0 for state in hands_history: ai_move = ai_decision_fn(state.hand, state.last_move) human_move = state.human_move if ai_move is None and human_move is None: hit += 1 elif ai_move and human_move and same_move_strength(ai_move, human_move): hit += 1 total += 1 return hit / total这个指标不用追求 100%,能到 60% 以上就说明策略层已经把牌型结构理解得比较到位了。这个评估脚本挂在你的策略接口上,每次修改出牌逻辑后都跑一遍,能避免改出“看起来合理但实际变笨”的回归问题。回放数据的来源不限于网上的公开牌局,你自己和朋友们打的每一局都可以存下来积累成评估集。
7.2 基于规则的对战测试:固定对手与胜负指标
离线评估之后,更进一步的验证方式是做 AI 对 AI 的对战测试。设计三个固定策略的机器人:一个是纯随机出牌(最弱),一个是贪心出最大牌(无脑强),一个是跟牌优先出最小的保守型(中等)。你的 AI 分别和这三个对手各打 100 局,统计胜率。这个测试的意义在于:胜率能反映你 AI 的绝对水平梯度,而且对手固定,排除了随机因素的干扰。
python selfplay_test.py \ --ai_model runs/training/card_v1/weights/best.pt \ --opponent random --games 100 \ --log results/selfplay_random.json注意事项:对战测试的环境需要把完整牌局逻辑实现出来——发牌、叫地主、出牌顺序、牌型判定、胜负结算。这套环境在 5.2 节其实已经搭了一半,enumerate_moves里已经包含了牌型判定逻辑,补上发牌和轮转就能跑起来。对局日志记录每一手的出牌、剩余手牌数和最终胜负,输掉的局重点看是策略问题还是识别问题——如果 AI 的手牌识别错了,那是模型问题;如果识别对了还输,那才是策略问题。这个区分非常重要,别把两个环节的问题混在一起调。
7.3 进阶方向:把检测输出接入强化学习
跑通上面整条链路后,如果你想继续提升棋力,下一步就是替换策略层。常见路径是接入 DQN 或类似算法,但这里有个血泪教训:直接用原始手牌状态做输入,学习效率极低——手牌排列组合数量太大,状态空间爆炸。我建议先把手牌转换成高度抽象的特征:每张牌的数值向量(一维 15 类)、上家剩余牌数、队友剩余牌数、是否叫地主等,组成一个固定长度的状态向量,再喂给 DQN。训练环境用 7.2 节搭的完整牌局逻辑,奖励函数简单一点:赢了 +1,输了 -1,或者用每局的剩余手牌数做稀疏奖励。训练过程中千万别忘了调交通期:初期让 AI 随机探索,后期逐渐减少随机率,否则它永远学不会出牌。
这个方向做完,你的框架就有了完整的三层结构:yolov5 负责看牌,规则基负责保证不出非法牌,强化学习负责做决定。视觉层换一个场景就重新训练数据,策略层则可以作为独立模块持续迭代。我自己跑完整套链路后最大的感悟是:yolov5 在这个项目里是把复杂问题简单化的关键支点,它让原本门槛很高的博弈 AI 变成了一条条清晰可执行的工程步骤,每一步都能单独验证、单独调优。希望这篇踩坑记录能帮你少走一段弯路。
本文还有配套的精品资源,点击获取