本文档介绍如何将 DouZero 深度强化学习模型集成到斗地主游戏中。
DouZero 是快手团队开发的斗地主 AI,使用深度强化学习训练。它在 2021 年的斗地主比赛中取得了优异成绩。
- 浏览器推理: ONNX.js 允许在浏览器中运行模型
- 跨平台: ONNX 支持多种运行时环境
- 优化推理: ONNX 提供了优化的推理引擎
- 模型压缩: 支持 INT8 量化,减小模型体积
┌─────────────────┐
│ PyTorch │
│ Checkpoint │
└────────┬────────┘
│
│ convert_douzero_to_onnx.py
│
▼
┌─────────────────┐
│ ONNX 模型 │
│ (3个文件) │
└────────┬────────┘
│
│ 部署到服务器
│
▼
┌─────────────────┐
│ 浏览器 │
│ ONNX.js │
│ 推理引擎 │
└─────────────────┘
pip install torch onnx onnxruntime numpy从官方仓库下载预训练模型:
git clone https://github.com/kwai/DouZero.git
cd DouZero
# 下载预训练权重
# landlord.pth, landlord_up.pth, landlord_down.pth使用提供的转换脚本:
cd xiangqi/scripts
# 转换所有模型
python convert_douzero_to_onnx.py \
--checkpoint_path /path/to/DouZero/checkpoints \
--output_dir ../public/models/douzero \
--quantize
# 或转换单个模型
python convert_douzero_to_onnx.py \
--checkpoint_path landlord.pth \
--output_dir ../public/models/douzero \
--model_type landlord转换完成后会生成以下文件:
public/models/douzero/
├── landlord.onnx # 地主模型 (~1.1MB)
├── landlord_up.onnx # 农民(上家)模型 (~1.1MB)
├── landlord_down.onnx # 农民(下家)模型 (~1.1MB)
├── landlord_int8.onnx # 量化后的地主模型 (~300KB)
├── landlord_up_int8.onnx # 量化后的农民模型
└── landlord_down_int8.onnx
转换脚本会自动验证输出一致性:
python convert_douzero_to_onnx.py \
--checkpoint_path ./checkpoints \
--output_dir ./onnx_models \
--skip_validation false将转换后的 ONNX 模型放到项目的 public 目录:
mkdir -p public/models/douzero
cp *.onnx public/models/douzero/模型路径在 model-loader.ts 中配置:
const MODEL_BASE_URL = '/models/douzero';npm install onnxruntime-web在 vite.config.ts 中添加 ONNX 文件支持:
export default defineConfig({
assetsInclude: ['**/*.onnx'],
});import { initializeDouZeroAI } from './doudizhu/game/ai';
// 初始化并加载模型
await initializeDouZeroAI((state) => {
console.log('AI 状态:', state);
// idle -> loading -> ready
});import { decidePlayAsync } from './doudizhu/game/ai';
const play = await decidePlayAsync(
hand, // 手牌
lastPlay, // 上一次出牌
position, // 玩家位置
isLandlord, // 是否是地主
'hard', // 难度
partnerRemaining, // 队友剩余牌数
landlordRemaining, // 地主剩余牌数
playHistory, // 出牌历史
landlordPosition // 地主位置
);import { ModelLoadingIndicator } from './doudizhu/components/ModelLoadingIndicator';
function GameScreen() {
const [modelLoaded, setModelLoaded] = useState(false);
return (
<>
{!modelLoaded && (
<ModelLoadingIndicator
onLoadComplete={() => setModelLoaded(true)}
onLoadError={(error) => console.error(error)}
/>
)}
{/* 游戏界面 */}
</>
);
}import { loadModel, selectBestAction } from './doudizhu/onnx';
// 加载单个模型
const session = await loadModel('landlord', {
onProgress: (progress) => {
console.log(`加载进度: ${progress.percentage}%`);
},
useCache: true,
});
// 手动推理
// ... (详见 API 参考)初始化 DouZero AI 实例。
参数:
onStateChange?: (state: AIState) => void- 状态变化回调
返回:
Promise<DouZeroAI>- AI 实例
示例:
const ai = await initializeDouZeroAI((state) => {
if (state === 'ready') {
console.log('模型加载完成');
}
});异步版本的出牌决策,支持 DouZero AI。
参数:
hand: Card[]- 手牌lastPlay: LastPlay- 上一次出牌position: PlayerPosition- 玩家位置isLandlord: boolean- 是否是地主difficulty: Difficulty- 难度partnerRemaining: number- 队友剩余牌数landlordRemaining: number- 地主剩余牌数playHistory: PlayHistoryEntry[]- 出牌历史landlordPosition?: PlayerPosition- 地主位置
返回:
Promise<Card[] | null>- 出牌决策
加载单个 ONNX 模型。
参数:
modelType: 'landlord' | 'landlord_up' | 'landlord_down'- 模型类型options?: ModelLoadOptions- 加载选项
ModelLoadOptions:
interface ModelLoadOptions {
onProgress?: (progress: LoadProgress) => void;
useCache?: boolean;
}返回:
Promise<ort.InferenceSession>- ONNX 推理会话
预加载所有模型。
参数:
onProgress?: (model, progress) => void- 进度回调
返回:
Promise<Map<ModelType, ort.InferenceSession>>- 会话映射表
清除所有缓存。
返回:
Promise<void>
编码地主观察。
参数:
obs: LandlordObservation- 观察对象
返回:
{ z: number[][][], x: number[][] }- 模型输入
编码农民观察。
参数:
obs: FarmerObservation- 观察对象
返回:
{ z: number[][][], x: number[][] }- 模型输入
将牌列表编码为 54 维向量。
参数:
cards: Card[]- 牌列表
返回:
number[]- 54 维向量
选择最优动作。
参数:
prediction: QValuePrediction- 预测结果
返回:
Card[]- 最优动作
带探索的动作选择。
参数:
prediction: QValuePrediction- 预测结果epsilon: number- 探索概率 (0-1)
返回:
Card[]- 选择的动作
获取 Top-K 动作。
参数:
prediction: QValuePrediction- 预测结果k: number- 数量
返回:
Array<{ action: Card[], qValue: number, rank: number }>- Top-K 结果
问题: 控制台显示 "Failed to fetch model"
解决方案:
- 检查模型文件是否存在于
public/models/douzero/ - 检查文件路径是否正确
- 检查文件权限
ls -la public/models/douzero/问题: 浏览器崩溃或内存警告
解决方案:
- 使用量化模型减少内存占用:
const session = await loadModel('landlord', {
useCache: true,
});
// 使用 *_int8.onnx 模型- 只加载需要的模型:
// 只加载地主模型
const session = await loadModel('landlord');问题: 每次推理耗时超过 1 秒
解决方案:
- 启用 WASM 多线程:
ort.env.wasm.numThreads = 4;- 使用缓存:
const session = await loadModel('landlord', {
useCache: true,
});- 使用 WebGPU (如果支持):
const session = await ort.InferenceSession.create(buffer, {
executionProviders: ['webgpu', 'wasm'],
});问题: TypeScript 编译错误
解决方案:
- 安装类型定义:
npm install @types/onnxruntime-web- 检查导入路径:
import * as ort from 'onnxruntime-web';问题: 模型更新后仍使用旧版本
解决方案:
清除缓存:
import { clearModelCache } from './doudizhu/onnx';
await clearModelCache();
location.reload();或在浏览器中手动清除:
- Chrome: 开发者工具 -> Application -> Cache Storage
- Firefox: 开发者工具 -> 存储 -> 缓存
使用 INT8 量化减小模型体积:
python convert_douzero_to_onnx.py \
--checkpoint_path ./checkpoints \
--output_dir ./onnx_models \
--quantize模型大小对比:
- 原始模型: ~1.1MB
- 量化模型: ~300KB
使用 CacheStorage 缓存模型:
// 首次加载
const session = await loadModel('landlord', {
useCache: true,
});
// 后续加载 (从缓存读取)
const session = await loadModel('landlord', {
useCache: true,
});并行加载所有模型:
const sessions = await preloadAllModels((model, progress) => {
console.log(`${model}: ${progress.percentage}%`);
});完整的示例代码位于:
xiangqi/src/doudizhu/
├── onnx/
│ ├── model-loader.ts # 模型加载器
│ ├── state-encoder.ts # 状态编码器
│ ├── action-decoder.ts # 动作解码器
│ ├── douzero-ai.ts # AI 主类
│ └── index.ts # 导出模块
├── game/
│ └── ai.ts # AI 集成
└── components/
└── ModelLoadingIndicator.tsx # UI 组件
- ✅ 完成 ONNX 转换脚本
- ✅ 实现浏览器推理引擎
- ✅ 添加模型缓存支持
- ✅ 实现 INT8 量化
- ✅ 添加降级策略
- ✅ 完成类型检查
欢迎提交 Issue 和 Pull Request!
在提交代码前,请确保:
- 运行类型检查:
npm run check - 运行代码风格检查:
npm run lint - 测试所有功能正常
本项目采用 MIT 许可证。
DouZero 模型版权归快手团队所有。