news 2026/10/28 7:40:59

C++抽卡模拟器:四层概率模型与蒙特卡洛校验

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++抽卡模拟器:四层概率模型与蒙特卡洛校验

简介:这份《原神抽卡模拟器.zip》面向希望用C++复刻游戏祈愿流程的学习者与原神玩家,提供无需消耗游戏货币即可体验角色、武器抽取过程的练习项目,重点在于把抽卡概率、保底规则与随机数生成转化为可运行的程序逻辑。压缩包共38个文件、约4.04MB,包含cpp、hpp与h源码,txt、tlog日志,sln、vcxproj工程配置,以及exe、pdb等编译调试产物,便于查看工程结构与运行结果。已有1606人学习下载。读者可从中参考祈愿池数据组织、随机抽取实现、保底计数器设计、抽卡记录与出货率统计,以及登录和MD5相关模块的写法;配套说明文档与日志也有助于理解项目构建、运行输出和调试过程,适合作为C++面向对象、容器与随机数库的综合作业或课程设计参考。

1. 一个 C++ 抽卡模拟器能有多较真:从压缩包里翻出的四层概率模型

把「十连必出四星」写成一句if的人,最后都会在统计那一步翻车。这个 C++ 抽卡模拟器压缩包解开后东西不算多:一份卡池配置、一个抽卡核心、一层控制台界面,加上存档和统计。它干的事很具体——把公告里那句「基础出率 0.6%,含保底综合 1.6%」拆成能被编译器执行、能被蒙特卡洛验证的状态机。适合三类人:想练 C++ 工程化又不想写图书管理系统的、要给自己的项目做数值原型的、以及被「为什么八十抽还不出来」折磨过想亲手算一遍期望的。下面按拆包复现的顺序讲。

2. 卡池与概率模型:把公告里的「出率」拆成四层状态机

新手最容易犯的错,是把「五星概率 0.6%」当成一个常数写死在代码里,然后拿if (r < 0.006)一路判到底。这样写出来的东西,单抽看着没问题,跑一万次统计就露馅:综合出率永远对不上 1.6%。原因不在随机数,在模型——真实的卡池从来不是单一概率,而是基础出率、软保底、硬保底、大小保底四层叠起来的条件概率,每一抽的实际概率都依赖你之前垫了多少。

2.1 四层状态拆解:每一抽的概率都是「上一次」的函数

先把四层说清楚,这是整个项目的地基:

第一层是基础出率,也就是不触发任何保底时的原始概率,通常是个很小的数,0.5%~0.8% 这一档。第二层是软保底,从第 N 抽开始,每多抽一次概率就往上加一档,常见做法是从第 74 抽起每次加 6%,一直加到硬保底。第三层是硬保底,抽到这个数必出,概率直接等于 1,不再走随机判定。第四层是大小保底,决定出的是不是「本期角色」,本质是一个布尔状态机:歪了下次必中,中了下次回到 50/50。

这四层不是并列的,是串联的。所以正确的实现顺序永远是:先算这一抽的实际五星概率 → 判定是否出五星 → 出五星后再判定是否 UP → 重置/递增各级计数器。顺序错了,怎么调参都对不上。

常见的参数表长这样,具体数值都能在配置文件里改,下面的典型值只是我拿来跑通验证的一组:

字段含义典型值改动后的影响
baseRate未触发软保底的单抽五星率0.006直接决定期望抽数的主要部分
softPityStart从第几抽开始概率爬升74调小会让综合出率虚高,调大则方差变大
softPityStep每抽爬升的绝对增量0.06太大三抽就满概率,太小硬保底前基本不变
hardPity必出的抽数上限90必须严格大于softPityStart,否则软保底是死代码
upRate出五星时命中 UP 的概率0.5影响「大保底」期望,不影响五星期望
fourStarPity四星保底抽数10与十连保底逻辑强耦合
fourStarRate四星基础出率0.051要和五星概率做互斥处理

注意,五星和四星不是独立事件。四星的判定必须发生在「这一抽没出五星」之后,否则你的四星出率会变成p4 × (1 - p5)的隐含值,统计出来永远偏低一点点——这点偏差在十万次采样下非常明显。

2.2 用配置结构体代替 if-else 硬编码

拆包后我最先动的地方,是把散落在draw()里的魔法数字全抽出来,做成一个PoolConfig。理由很实际:这类模拟器九成的改动需求都是「换个卡池参数再跑一遍」,如果参数写死在分支里,改一次就要全局搜一遍0.006,漏一处就是一次血泪经验。

#include <algorithm> #include <cmath> // 卡池参数:所有数值都可以从配置文件读入,代码里不出现硬编码 struct PoolConfig { double baseRate = 0.006; // 未触发软保底的单抽五星率 int softPityStart = 74; // 从第几抽起概率开始爬升 double softPityStep = 0.06; // 每抽爬升的绝对增量 int hardPity = 90; // 硬保底,必出 double upRate = 0.5; // 出五星时命中 UP 的概率 int fourStarPity = 10; // 四星保底抽数 double fourStarRate = 0.051; // 四星基础出率 }; // pity 表示"距离上次五星已经抽了多少次",第一次抽时 pity == 0 // 返回值是"这一抽"的实际五星概率,区间 [0, 1] inline double currentFiveStarRate(const PoolConfig& cfg, int pity) { const int nth = pity + 1; // 本次是第几抽 if (nth >= cfg.hardPity) return 1.0; // 硬保底:直接满概率 if (nth < cfg.softPityStart) return cfg.baseRate; const int over = nth - cfg.softPityStart + 1; // 超出软保底起点的抽数 return std::min(1.0, cfg.baseRate + cfg.softPityStep * over); }

这段代码有三个参数值得盯住。

pity的语义是「已经抽了几次还没出」,所以判定时用的是pity + 1。这个+1是经典翻车点:写成pity会导致软保底整体延后一抽,硬保底变成 91 抽才出,而单抽看代码完全看不出来,只有跑到第 90 抽才暴露。

硬保底那一行必须是return 1.0,不能走std::min算出来的近似值。因为后续判定用的是r < p,而r的取值是[0, 1),理论上p = 1.0时必然成立,但只要你前面有任何一步浮点误差把上限压到 0.9999999,就存在极小概率漏判——这种 bug 一年可能遇到一次,遇到一次你能查三天。

std::min兜底是为了防止参数被改坏。有人把softPityStep调到 0.2,第 80 抽概率就超过 1 了,discrete_distribution拿到大于 1 的权重不会报错,只会静默地改变分布,非常难查。

2.3 概率表自检:累计和、单调性与边界

配置化之后紧接着就是校验。我在入口处加了一个validate(),任何一条不满足就直接拒绝启动,而不是带着错误的表跑出「看起来正常」的结果。

#include <numeric> #include <string> #include <vector> // 星级内部各物品的权重表,返回 true 表示可用 bool validateWeights(const std::vector<double>& w, std::string& why) { if (w.empty()) { why = "权重表为空"; return false; } double sum = std::accumulate(w.begin(), w.end(), 0.0); // 手写小数很容易凑出 0.998 或 1.002,容差放到 1e-6 足够 if (std::fabs(sum - 1.0) > 1e-6) { why = "累计和不为 1,实际为 " + std::to_string(sum); return false; } for (double x : w) { if (x < 0.0) { why = "存在负权重"; return false; } } return true; }

累计和这条卡得最严。我见过一个卡池把五个角色的权重写成0.3 / 0.25 / 0.2 / 0.15 / 0.1,看着是 1.0,浮点加起来是0.9999999999999999,discrete_distribution会把它当成合法输入,最后一个角色永远抽不到——因为它的区间被压缩到了浮点精度以下。

单调性检查针对的是currentFiveStarRate:写个循环从pity = 0跑到hardPity,断言rate(n+1) >= rate(n),并且最后一项必须等于 1.0。这条断言拦住过一次我把softPityStart和hardPity配反的低级错误,当时程序照样能跑,只是出率曲线是个倒 V 形。

提示:validate()只在 Debug 构建里跑全量检查,Release 里保留累计和那一项即可,否则每次启动都要遍历几千个权重,控制台启动会有明显停顿感。

3. 随机数引擎:把rand()换成 mt19937 之后概率才第一次对上

概率模型修好之后,我跑了一百万次采样,结果综合出率 1.58%,比理论值低了一点点。差得不多,但方向是单向的,不像随机波动。查了两小时,问题出在rand() % 100 < 1这一句上——这也是绝大多数 C++ 小项目里的默认写法。

3.1rand() % n的三宗罪

第一宗是取模偏置。RAND_MAX在不同平台上是 32767 或 2147483647,取模之后前面若干个桶会比后面的桶多分到一个值。拿% 100举例,2147483648 % 100 = 48,于是 0~47 这 48 个桶各多出一次命中机会,偏差量级是1/21474836。听起来微不足道,但配合「一抽 0.6%」这种小概率,相对误差就被放大了——你实际跑出来的五星率会比 0.6% 高出万分之一左右,恰好就是观测到的那个偏移。

第二宗是低位周期短。很多实现的rand()低位比特周期性极差,而% n恰好只保留低位。表现是:把种子固定之后,连续抽卡会出现明显的规律性重复,做「十连结果」这种成组输出时尤其扎眼。

第三宗是不可复现。rand()用全局状态,你没法在存档里保存「当前随机数流走到哪了」。玩家退出再进,随机序列就断片了,想复现某次抽卡结果做客服核查,根本做不到。

对比项rand() % nmt19937_64+ 标准分布
取模偏置有,n不整除RAND_MAX+1时必然存在无,分布库内部做了拒绝采样
周期实现相关,常见 2^31 量级2^19937-1
状态可序列化否,全局状态是,引擎对象可整体存档
跨平台一致性否,RAND_MAX与实现有关是,标准规定了输出序列
单次耗时更快慢 2~5 倍,抽卡场景完全无感

最后一行是很多人纠结的点。抽卡模拟器一秒钟最多跑几千次,mt19937的开销在整条链路上根本排不进前三,相比之下std::string拼接和std::cout同步才是大头。为了省这点时间换来一个没法复现的结果,不划算。

3.2mt19937_64与分布库的标准写法

我一般把引擎包一层薄壳,只暴露三个能力:取[0,1)的浮点、按权重取离散值、导出当前种子。这样上层代码不需要知道底下用的是哪个引擎,将来换pcg64之类也只是一处改动。

#include <cstdint> #include <random> #include <vector> class Rng { public: explicit Rng(uint64_t seed) : seed_(seed), eng_(seed) {} // [0, 1) 均匀浮点,用于概率判定 double uniform01() { return std::generate_canonical<double, 53>(eng_); } // 按权重取下标,weights 必须已通过 validateWeights // 分布对象按需构造,权重稳定时可缓存到成员里 size_t pick(const std::vector<double>& weights) { std::discrete_distribution<size_t> dist(weights.begin(), weights.end()); return dist(eng_); } uint64_t seed() const { return seed_; } private: uint64_t seed_; std::mt19937_64 eng_; };

generate_canonical<double, 53>的第二个模板参数是有效位数,53 是double尾数位宽,取 53 意味着用尽全部精度组合出[0,1),不会出现「概率小于 2^-32 的事件永远不触发」这种隐形下限。有人图省事写eng_() / (double)UINT64_MAX,结果能取到 1.0,r < p在p = 1.0时不成立,硬保底又漏了。

std::discrete_distribution每次构造都要对权重做一次前缀和,十连抽十次就是十次重建。物品数量上百时这点开销会开始显现,解决办法是把分布对象提到Pool里缓存,权重变化时才重建。

注意:discrete_distribution对权重和非常敏感。传入{0.3, 0.25, 0.2, 0.15, 0.1}时它会自己归一化,所以即使和是 0.9999 也能跑,但归一化之后你原本设计的比例已经被悄悄改了。校验那一步不能省。

3.3 种子管理:把 seed 写进存档而不是只打印

可复现的关键在种子。设计上有两个选择:要么用std::random_device取真随机种子,要么让玩家手动指定。我一般两个都留——正常游玩走random_device,调试和出问题时走手动指定。

#include <random> Rng makeRng(uint64_t forcedSeed, bool useForced) { // random_device 在部分平台上可能退化为确定性实现, // 所以额外混入时间戳作为兜底,保证两次启动不会撞种子 uint64_t s = forcedSeed; if (!useForced) { std::random_device rd; s = (static_cast<uint64_t>(rd()) << 32) ^ rd(); s ^= static_cast<uint64_t>(std::chrono::steady_clock::now().time_since_epoch().count()); } return Rng(s); }

Rng里那个seed_成员容易被忽略,但它是存档的必需品:只存「种子」不存「引擎当前状态」,读档后随机序列会从头开始,等于把已经抽过的卡再抽一遍。要真正做到断点续抽,得序列化引擎状态(std::mt19937_64支持<<和>>,输出是一长串十进制数),或者更简单的做法——每次抽卡前把当前引擎状态连同pity5、pity4、guaranteedUp一起快照到存档里。文本格式可以读,二进制格式体积小,抽卡这种数据量我倾向用文本,出问题时能直接肉眼核对。

4. 抽卡核心:单抽、十连和保底记录记账的代码骨架

模型清楚了、随机数干净了,剩下的就是把这套东西落到一个函数上。这一步看着简单,实际是整个包里改动最频繁的地方,因为所有规则都在这里汇合。

4.1GachaState:把「垫了多少抽」变成可存可读的结构体

状态结构体的第一原则是:所有会跨抽次携带的东西都必须在这里,不能藏在函数静态变量里。

#include <cstdint> #include <string> struct GachaState { int pity5 = 0; // 距上次五星已抽次数 int pity4 = 0; // 距上次四星已抽次数 bool guaranteedUp = false; // 大保底:下次五星必为 UP uint64_t seed = 0; // 本次会话的种子,用于复现 uint64_t draws = 0; // 累计抽数,统计用 }; struct DrawResult { int star = 3; // 3 / 4 / 5 bool isUp = false; // 仅五星有意义 std::string name; // 物品名 };

guaranteedUp这个布尔量是大小保底的全部实现。它只有两个转换:出五星且isUp == false时置true,出五星且isUp == true时置false。中间那些没出五星的抽次一次都不能碰它,否则大保底会被莫名其妙地清掉。

draws看着是冗余字段,其实是排查问题的抓手。每次统计异常,第一件事就是拿draws和日志条数对一遍,对不上说明有分支提前return了。

4.2 单抽函数:先定星级,再定物品

顺序问题在这里体现得最明显。正确的做法是两级抽签:第一次抽签决定星级,第二次抽签在星级内部决定具体物品。之所以不能一把梭,是因为星级内部的权重是各自归一化的,跟星级概率是两套数。

DrawResult drawOne(GachaState& st, const PoolConfig& cfg, Rng& rng, const Pool& pool) { st.draws++; // 第一级:判定星级 const double p5 = currentFiveStarRate(cfg, st.pity5); if (rng.uniform01() < p5) { // 大保底优先:上一轮歪了这次必中,不再掷骰 const bool up = st.guaranteedUp || (rng.uniform01() < cfg.upRate); st.guaranteedUp = !up; // 歪了置 true,中了置 false st.pity5 = 0; st.pity4++; // 五星不和四星保底共用计数器,但仍要推进 return DrawResult{5, up, pool.pickFiveStar(up, rng)}; } st.pity5++; // 第二级:四星判定,只在没出五星时进行 const double p4 = (st.pity4 + 1 >= cfg.fourStarPity) ? 1.0 : cfg.fourStarRate; if (rng.uniform01() < p4) { st.pity4 = 0; return DrawResult{4, false, pool.pickFourStar(rng)}; } st.pity4++; return DrawResult{3, false, pool.pickThreeStar(rng)}; }

几个参数和细节说明。

GachaState&必须是引用。写成传值的话,函数内所有计数改动出了作用域就丢了,表现是「保底永远不生效、每一抽都按基础出率算」,综合出率会稳定在 0.6% 左右,跟理论值差三倍,很好认。

st.guaranteedUp || (...)用的是短路求值,大保底状态下根本不会消耗随机数。这一点在复现时很重要——如果改成先掷骰再判断,同样的种子会走出不同的序列,日志对不上。

四星判定里那个st.pity4 + 1 >= cfg.fourStarPity,+1的道理和五星那边一样。出五星时pity4自增而不是清零,是因为四星保底只被「出四星」这件事重置,五星不占用四星保底名额。这个设计各家不一样,如果你的规则是「五星也算四星保底」,那就在五星分支里把pity4也清零,但要保证十连保底逻辑跟着改。

4.3 十连:计数不能重置,保底要回写

十连最常见的错误写法,是在循环里每次都新建一个GachaState。这么写出来的十连,等价于十次独立单抽,四星保底形同虚设。

#include <vector> std::vector<DrawResult> drawTen(GachaState& st, const PoolConfig& cfg, Rng& rng, const Pool& pool) { std::vector<DrawResult> out; out.reserve(10); // 关键:十次抽卡共用同一个 state,计数器连续推进 for (int i = 0; i < 10; ++i) { out.push_back(drawOne(st, cfg, rng, pool)); } // 十连保底:整组没有四星及以上时,把最后一抽升级 bool has4Plus = false; for (const auto& r : out) { if (r.star >= 4) { has4Plus = true; break; } } if (!has4Plus) { out.back().star = 4; out.back().isUp = false; out.back().name = pool.pickFourStar(rng); st.pity4 = 0; // 升级同样消耗了四星保底,计数必须回写 } return out; }

这段里最容易漏的是最后那行st.pity4 = 0。升级动作发生在drawOne之外,如果不手动回写,状态里还记着「已经垫了 10 抽没出四星」,下一抽的四星概率直接顶到 1.0,等于白送一个四星。这个 bug 在单次十连里看不出来,只有连续抽几十次才会暴露。

另一个边界是「升级的最后一抽如果本来就是四星」——那种情况下has4Plus早就是true,根本不会进升级分支,所以不用担心覆盖掉已有结果。但如果你的保底规则是「十连必出四星,且最后一抽不低于四星」,那就得单独处理,别直接套这段。

4.4 存档:文本序列化比二进制省事

抽卡模拟器的存档数据量极小,几十个字节,用文本最划算。

#include <fstream> #include <sstream> // 存档格式:每行 key=value,方便肉眼核对和手工改 bool saveState(const GachaState& st, const std::string& path) { std::ofstream f(path); if (!f) return false; f << "seed=" << st.seed << "\n" << "draws=" << st.draws << "\n" << "pity5=" << st.pity5 << "\n" << "pity4=" << st.pity4 << "\n" << "guaranteedUp=" << (st.guaranteedUp ? 1 : 0) << "\n"; return static_cast<bool>(f); } bool loadState(GachaState& st, const std::string& path) { std::ifstream f(path); if (!f) return false; std::string line; while (std::getline(f, line)) { std::istringstream is(line); std::string key; if (!std::getline(is, key, '=')) continue; std::string val; std::getline(is, val); if (key == "seed") st.seed = std::stoull(val); else if (key == "draws") st.draws = std::stoull(val); else if (key == "pity5") st.pity5 = std::stoi(val); else if (key == "pity4") st.pity4 = std::stoi(val); else if (key == "guaranteedUp") st.guaranteedUp = (val == "1"); } return true; }

std::stoi和std::stoull在输入不合法时会抛std::invalid_argument,存档被手改坏就会崩。稳妥的做法是包一层try/catch,解析失败就当作新档处理,而不是让程序直接退出。这一点在给别人分发的时候尤其重要——总有人会去改pity5试试能不能跳过保底。

5. 避坑实况:五个把概率算错的现场和它们的排查路径

下面这五条都是我在复现和改参数时真实撞上的。顺序按「从最容易发现到最难发现」排,最后一条花了我一整个下午。

5.1 出率统计偏低:概率表累计和不是 1

现象:跑十万次采样,各星级分布加起来是 0.9987,少了大概 0.13%,且缺失的部分全部集中在权重最小的那个物品上,它一次都没出现过。

原因:手写权重时凑了一组0.32 / 0.28 / 0.20 / 0.12 / 0.06 / 0.02,看着是 1.0,实际浮点累加是 0.9999999999999998。discrete_distribution内部做前缀和后,最后一个区间的上界恰好等于自己的下界,长度为零。

解决:在加载配置后立刻调用validateWeights,容差 1e-6,不通过就拒绝启动并打印实际和。同时给每个权重加一个「最小可见权重」下限,低于 1e-6 的直接判为配置错误——这种量级的权重本来也不可能被抽到,留着只会制造疑惑。

5.2 十连出两个五星:状态被复制而不是引用

现象:单抽完全正常,一进十连就出问题,偶尔一组十连里冒出两个五星,概率上极不合理。

原因:drawTen内部写了auto s = st;,本意是「复制一份临时的」,结果十次循环都在这份副本上累加,每次循环开头又没重置,逻辑上变成了十次独立判定,同时还把外层的st保护得死死的,pity5从头到尾都是 0。

解决:把drawOne和drawTen的状态参数统一改成GachaState&,并且在函数签名上就不接受按值传递。更进一步,把GachaState的拷贝构造delete掉,编译期直接堵死这条路。

5.3 每次启动抽卡序列一模一样:种子没初始化

现象:关掉程序重开,前二十抽的结果和上次完全一致,连物品名都一样。

原因:代码里留着std::srand(std::time(nullptr));的历史遗留,但抽卡逻辑已经换成了mt19937,而mt19937的构造函数如果传固定常量(比如Rng rng(42)),每次启动都会从同一个位置开始。

解决:抽卡引擎的种子走random_device加时间戳混合,同时保留手动指定入口。改完之后顺手把rand()相关的调用全部删掉——留着迟早有人误用。

5.4 硬保底第 90 抽还是不出:保底靠概率判定

现象:日志里清楚地记录着pity5 = 89,下一抽判完还是三星。

原因:currentFiveStarRate在硬保底那一档返回的是std::min(1.0, ...)算出来的值,而某次改参数把softPityStep调成了 0.05,理论上第 90 抽是0.006 + 0.05 * 17 = 0.856,根本没到 1.0。代码里没有单独的「硬保底直接返回」分支,全部依赖软保底爬升自然触顶。

解决:在currentFiveStarRate最前面加一条if (nth >= cfg.hardPity) return 1.0;,让硬保底成为一个独立分支而不是软保底的推论。同时在validate()里断言「软保底爬到硬保底时必须已经 >= 1.0」,两条路互为保险。

5.5 控制台卡名全是问号:UTF-8 与代码页不一致

现象:g++编译一切正常,运行起来中文物品名显示成????,五星和三星的区分度直接没了。

原因:源文件保存为 UTF-8,字符串字面量也是 UTF-8 字节序列,但 Windows 控制台默认代码页是 936(GBK),std::cout原样输出字节流,终端按 GBK 解码就全乱了。

解决:程序启动时调一次SetConsoleOutputCP(CP_UTF8),并把std::cout的 locale 设成".UTF-8"。跨平台的话用条件编译,非 Windows 平台这行跳过。如果不想引入平台相关代码,退而求其次把源码保存成 GBK,但这会在 Linux 上反过来出问题,不推荐。

排查入口先看什么常见结论
综合出率对不上权重累计和、软保底爬升曲线配置错,不是随机数错
保底不生效GachaState是否按引用传递传值导致计数丢失
结果不可复现种子来源、引擎是否被重新构造固定种子或未序列化引擎状态
中文乱码源文件编码 vs 控制台代码页输出前设置代码页

6. 用蒙特卡洛把概率表验到底:一百万次抽卡的校验习惯

改完上面这些,我一般不会急着看界面,而是先跑一个无界面的校验程序:直接构造GachaState和Rng,循环抽一千万次,只统计三件事——五星总数、UP 命中数、平均每五星抽数。

#include <cstdio> void monteCarlo(const PoolConfig& cfg, const Pool& pool, uint64_t n) { GachaState st; // 从零开始,模拟新号 Rng rng(20240501ULL); // 固定种子,跑两次结果必须一致 uint64_t five = 0, up = 0; for (uint64_t i = 0; i < n; ++i) { DrawResult r = drawOne(st, cfg, rng, pool); if (r.star == 5) { five++; if (r.isUp) up++; } } std::printf("五星出率 = %.6f%% 期望抽数 = %.2f UP 占比 = %.4f\n", five * 100.0 / n, five ? static_cast<double>(n) / five : 0.0, five ? static_cast<double>(up) / five : 0.0); }

跑一千万次大概几秒钟。判读结果有三条经验:五星出率应该落在 1.5%~1.7% 之间,低于 1.5% 说明软保底没生效,高于 1.8% 多半是硬保底分支被重复触发;期望抽数在 62~65 之间是正常区间,明显偏小要怀疑softPityStart被改过;UP 占比应该贴近 0.5,如果偏差超过 0.01,去查guaranteedUp有没有在某条分支里被意外清掉。

同样的种子连跑两次,结果必须逐位一致。这条我当成硬性验收标准——只要两次不一致,说明代码里还有没被状态覆盖的随机源,后面所有统计都不能信。真正的出率验证还要加一步卡方检验,把十万次结果按星级分桶算卡方值,自由度取桶数减一,p 值小于 0.01 就说明分布显著偏离,回去查权重表。

被这个 0.6% 坑过一次之后,我养成了一个习惯:任何涉及概率的改动,先跑固定种子的一百万次采样存档,改完再跑一遍,两份结果做 diff。肉眼看代码永远看不出「这一行少了个+1」,但统计数字会立刻告诉你。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 7:40:26

大模型推理加速实战:Eagle/MTP/DFlash/DSPark四大投机解码方案对比

1. 这不是“加速黑魔法”&#xff0c;而是大模型推理的理性突围你有没有遇到过这样的场景&#xff1a;刚部署好一个7B参数的开源模型&#xff0c;本地跑推理&#xff0c;每秒只能吐出2个token&#xff0c;等它写完一封邮件&#xff0c;咖啡都凉透了&#xff1b;或者在做实时对话…

作者头像 李华
网站建设 2026/10/28 7:40:08

Qt FTP文件传输实战:QFtp迁移、断点续传与PASV排错

简介&#xff1a;围绕 Qt 框架实现 FTP 文件传输的示例代码包&#xff0c;面向具备 C 基础、需要在桌面应用中集成远程文件上传下载能力的开发者。内容以 QFtp 类为主线&#xff0c;覆盖连接服务器并设置地址与端口、账号登录、get/put 上传下载、目录列举与切换、新建目录&…

作者头像 李华
网站建设 2026/10/28 7:38:40

Agent与LLM生态深度解析:RAG存储边界、GraphRAG落地与MCP协议实践

1. 从一份"日报"说起&#xff1a;Agent 与 LLM 生态到底在卷什么做 AI 应用开发这几年&#xff0c;我养成了一个习惯&#xff1a;每天早上花二十分钟扫一遍 Agent 和 LLM 领域的新动态。不是为了追热点&#xff0c;而是这个领域的迭代速度实在太快——上周还在用的方…

作者头像 李华
网站建设 2026/10/28 7:38:26

基于CNN的表情识别系统设计与优化实践

1. 项目背景与核心价值表情识别作为计算机视觉领域的重要分支&#xff0c;近年来在人机交互、智能安防、心理健康监测等领域展现出巨大应用潜力。基于CNN的解决方案因其出色的特征提取能力&#xff0c;成为该领域的主流技术路线。这个毕设选题完美结合了深度学习前沿技术与实际…

作者头像 李华
网站建设 2026/10/28 7:37:51

AI工程实战:更少token、更强模型与更难管的agent治理

这一周我大部分时间泡在AI工程的现场&#xff1a;白天帮业务方调agent的线上行为&#xff0c;晚上盯着token账单和数据看板复盘。说实话&#xff0c;这周的观感非常典型——更少token、更强模型、更难管的agent&#xff0c;三个趋势同时压过来&#xff0c;工程侧的压力和机会都…

作者头像 李华
网站建设 2026/10/28 7:37:50

C# 考勤机 SDK 二次开发:COM 组件对接与打卡数据增量采集

简介&#xff1a;面向中控考勤机二次开发者的综合资料包&#xff0c;围绕设备通信、接口调用与考勤数据管理展开&#xff0c;适合具备C#或VB.NET基础、需要快速接入中控硬件的开发者参考。包内共1264个文件、约11.08MB&#xff0c;以221个cs源码、214个dll库、133个exe程序及34…

作者头像 李华