尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

马尔可夫转移矩阵实战:从用户行为预测到工程落地

马尔可夫转移矩阵实战:从用户行为预测到工程落地 真正让我把马尔可夫转移矩阵当成一个“智能计算”问题来研究的是一次APP用户行为序列分析。当时手里的埋点日志里有一条条用户浏览路径A页面进来B页面停留C页面跳转有些人直接去了支付页有些人反复逛了一圈又回到首页。老板给的目标很直接能不能根据用户当前的访问状态预测他下一步最可能去哪儿以及未来三步之内有多大概率完成转化。我一开始想用枚举法把所有可能路径一条条列出来数出现频率。可状态的组合一多路径数量直接指数级暴涨还没等算完日志里又涌进来几十万条新数据。后来我意识到这个问题天生就是马尔可夫链的菜而它背后的转移矩阵本质上是在做一种可以靠线性代数高效求解的“智能计算”。这篇文章我想把马尔可夫转移矩阵从概念、数学原理到代码实现和工程落地完整拆一遍。不搞教科书式的定义堆砌而是从实际项目里遇到的问题出发讲清楚它为什么能“智能”以及我在实践过程中踩过的、容易让人崩溃的坑。1. 马尔可夫链到底在描述什么记忆只留在当前状态1.1 一个APP埋点数据引发的思考马尔可夫链描述的是一个有先后顺序的随机过程它的核心假设极其简单下一个时刻的状态只取决于当前时刻的状态跟更早的历史没有关系。数学上写出来就是P(X(t1) j | X(t) i, X(t-1), X(t-2), ...) P(X(t1) j | X(t) i)这个性质叫马尔可夫性质也叫“无后效性”或“无记忆性”。放到用户行为场景里就是用户在访问当前页面时下一步点哪儿的概率跟用户之前逛过哪些页面无关只看当前页面本身。我第一次听到这种假设时第一反应是这也太理想化了吧用户行为怎么可能没有记忆后来跑完数据才发现在大量真实场景里一阶马尔可夫假设虽然粗暴却足够捕捉主要规律尤其当状态空间设计得比较细致的时候效果远好于“啥都不假设”的暴力枚举。这个“只记当前状态”的设定正是马尔可夫链能被智能计算的前提。因为一旦把历史全部纳入考虑状态空间会膨胀成天文数字而把历史压缩成“当前状态”一阶信息之后整个世界变小了也就能用矩阵来精确刻画了。1.2 从状态转移图到转移矩阵马尔可夫链通常用一个状态空间和一套转移概率来定义。比如下面这个简单的天气模型状态空间只有两个状态晴S和雨R今天如果是晴天明天 80% 还是晴天20% 变成雨天今天如果是雨天明天 60% 转晴40% 还是雨天这两句话可以画成一张状态转移图节点是天气状态箭头上标注的是转移概率。状态多了之后画图不好维护更标准的做法是写成一个矩阵。我把行当成“当前状态”列当成“下一个状态”矩阵第 i 行第 j 列就表示当前状态 i - 下一个状态 j的概率晴(S) 雨(R) 晴(S) [ 0.8 0.2 ] 雨(R) [ 0.6 0.4 ]这个矩阵就是马尔可夫转移矩阵。它是整条链的核心几乎一切后续计算——多步转移概率、稳态分布、吸收概率——都建立在这一个矩阵上。1.3 行随机矩阵的两个硬性约束并不是随便一个矩阵都能当转移矩阵用。它必须满足两个条件非负性所有元素都大于等于 0。概率没有负的这是底线。行归一性每一行的元素之和等于 1。无论当前处于哪个状态下一个时刻总得落入某个状态不可能“凭空消失”。这两个条件合起来英文里叫stochastic matrix中文叫行随机矩阵。我在实际建模时经常在处理完缺失数据之后发现某一行不是严格等于 1比如 0.999999999 或者 1.0000000001这就是浮点运算的累积误差。虽然差别很小但如果后面要反复做矩阵乘法、幂运算误差会被放大。所以养成一个好习惯在矩阵构造完成后检查并强制做一次行归一化把误差收敛回来。2. 智能计算的第一层跃迁从穷举路径到矩阵连乘2.1 手算三步转移概率的代价如果不用矩阵咱们要算“三天之后是晴天的概率”会怎么做假设今天是晴天。明天可能是晴或雨后天又是晴或雨到大后天同样。于是所有可能的路径有 2×2×2 8 条。我必须把每条路径的概率乘起来再把“最终是晴天”的路径概率加起来晴-晴-晴-晴: 0.8 * 0.8 * 0.8 0.512 晴-晴-雨-晴: 0.8 * 0.2 * 0.6 0.096 晴-雨-晴-晴: 0.2 * 0.6 * 0.8 0.096 晴-雨-雨-晴: 0.2 * 0.4 * 0.6 0.048四条路径加起来0.512 0.096 0.096 0.048 0.752。看着还能手算但这里状态只有两个、步数只有三步。换成 10 个状态、推演 20 步路径数是 10^20 条这已经不是“要不要手算”的问题而是“用超算都算不完”的问题。穷举路径这条路走不通。2.2 Chapman-Kolmogorov 方程一句话讲透穷举不行就得找结构的便宜。这里有一个特别重要的定理叫Chapman-Kolmogorov 方程用大白话说就是从 i 走到 j走 mn 步的概率等于“先走 m 步到达某个中间状态 k再走 n 步从 k 到 j”的概率对所有中间状态 k 求和。写成矩阵就是P^(mn) P^m × P^n这玩意儿一出问题立刻从“枚举海量路径”变成了“矩阵乘法”。而矩阵乘法是现代计算机科学计算库最擅长的操作没有之一。于是从“今天的状态分布”预测未来 n 步的状态分布只需要做一次矩阵幂运算v(tn) v(t) × P^n其中 v(t) 是当前时刻的状态分布向量是一个行向量每一维对应某个状态的概率。2.3 矩阵连乘带来的向量化红利我最喜欢这个跃迁的一点是一旦把转移计算表达成矩阵乘法就能直接调用大量成熟的数值计算工具。底层有 BLAS、LAPACK 这些基础线性代数库做加速上层有 NumPy、SciPy 提供现成接口。还有更激进的方案用 GPU 做大规模矩阵乘法几千维的状态空间一样能推得飞快。所以“智能计算”这个词不是说算法有多玄而是指把指数级计算复杂度的路径枚举问题转化成多项式复杂度、甚至高度并行化的矩阵运算问题。这才是马尔可夫转移矩阵真正聪明的地方。3. Python 实操转移矩阵的构造与多步推演3.1 从原始序列构造转移矩阵工程里拿到手的通常不是矩阵而是很长很长的状态序列比如用户点击路径[home, search, detail, cart, pay]。下一步是从这些原始序列里统计出转移计数矩阵。直接上代码这个函数我几乎在所有相关项目里都会复用import numpy as np def build_transition_matrix(sequence, states): 从状态序列构建一阶马尔可夫转移矩阵。 sequence: list[str]状态序列 states: list[str]全部状态的有序列表 n len(states) state_index {s: i for i, s in enumerate(states)} count_matrix np.zeros((n, n)) for t in range(len(sequence) - 1): i state_index[sequence[t]] j state_index[sequence[t 1]] count_matrix[i, j] 1.0 # 行归一化注意处理全零行 row_sums count_matrix.sum(axis1, keepdimsTrue) transition np.divide( count_matrix, row_sums, outnp.zeros_like(count_matrix), where(row_sums 0) ) return transition注意我用了np.divide的where参数这是为了避免某个状态完全没有出边时除零产生 NaN。全零行在正常情况下不会发生但在数据质量差的项目里非常常见后面第 6 节我会详细展开。构造完转移矩阵后可以用一个小工具函数来展示矩阵def print_matrix(matrix, states, precision4): header .join([f{s:7} for s in states]) print(header) for i, s in enumerate(states): row .join([f{v:7.{precision}f} for v in matrix[i]]) print(f{s:5} {row})3.2 多步转移概率的批量计算有了基础转移矩阵 P要算 n 步转移概率最直接的方法就是矩阵幂运算。NumPy 提供了np.linalg.matrix_power内部还会做快速幂优化def multi_step_transition(P, steps): return np.linalg.matrix_power(P, steps)比如刚才的天气例子三步转移矩阵直接算P np.array([[0.8, 0.2], [0.6, 0.4]]) P3 multi_step_transition(P, 3) print(P3)输出会得到[[0.752 0.248] [0.744 0.256]]看左上角 0.752跟前面手算的路径枚举结果完全一致。这说明矩阵连乘和路径枚举在数学上是等价的但计算量天差地别。3.3 预测未来若干步的状态分布转移矩阵本身只是条件概率真正做预测时我还得考虑初始状态。假设今天是晴天的概率是 90%、雨天的概率是 10%写成向量v0 np.array([0.9, 0.1]) v3 v0 P3 print(v3) # [0.7512 0.2488]这表示三天后晴天概率约 75.12%雨天约 24.88%。如果要做一条未来趋势曲线就循环算每个时间步的分布def forecast_distributions(v0, P, horizon10): distributions [] v v0.copy() for _ in range(horizon 1): distributions.append(v.copy()) v v P return np.array(distributions)这个输出的每一行就是未来第 t 天状态分布的全过程。业务上能直接拿来画概率演化曲线比单一的一个“最终概率”信息量大得多。4. 稳态分布当转移矩阵收敛后的长期规律4.1 稳态分布到底解决什么问题很多业务问题其实问的不是“明天怎么样”而是“长期来看会稳定在哪里”。比如一个内容平台的用户长期下来分布在哪几类页面的比例是多少一个衰退期的产品如果把流失状态单独拎出来长期留存率会收敛到多少搜索引擎里网页长期被访问的比例如何分配答案就是稳态分布也叫平稳分布。它满足一个非常优雅的条件π πPπ 是一个行向量把转移矩阵 P 再作用一次之后分布依旧不变。换句话说当系统运行足够长时间状态分布会趋于一个固定值不再随时间变化这个固定值就是 π。4.2 幂迭代与特征值分解两种求法求稳态分布工程上最常用的有两种做法。第一种是幂迭代法思想特别直观随便给一个初始分布 v0反复乘 P直到变化量小于阈值。因为很多现实问题里的链都有收敛性乘到最后就会逼近 π。def stationary_distribution(P, tol1e-12, max_iter10000): n P.shape[0] v np.ones(n) / n # 均匀初始分布 for _ in range(max_iter): v_next v P diff np.linalg.norm(v_next - v) v v_next if diff tol: break return v / v.sum()第二种是特征值分解法。注意稳态条件是 π πP换个角度写就是 P^T π^T 1 × π^T这说明 π^T 是矩阵 P^T 的特征值为 1 对应的特征向量。所以可以用特征值分解来找eigvals, eigvecs np.linalg.eig(P.T) idx np.argmin(np.abs(eigvals - 1.0)) stationary np.real(eigvecs[:, idx]) stationary stationary / stationary.sum() print(stationary)这两种方法各有适用场景方法适用规模数值稳定性实现复杂度幂迭代法大矩阵、大规模稳定但收敛慢低特征值分解中小规模稠密矩阵对复杂特征值敏感中实际工程中如果状态空间是几百到几千维且是稠密矩阵np.linalg.eig没事但如果上几万维还稀疏直接特征值分解会非常痛苦我更推荐用 SciPy 的scipy.sparse.linalg.eigs只求特征值接近 1 的少数几个特征向量或者干脆用幂迭代。4.3 收敛前提不可约、非周期与实际取舍不是所有转移矩阵都会收敛到唯一的稳态分布。严格数学上要求三条不可约从任何状态出发都能经过有限步到达任何其他状态。如果存在一个“孤岛”状态系统可能分成多个互不相通的子集。非周期状态返回自身所需步数的最大公约数为 1。如果只按固定周期循环比如晴天雨天严格交替分布会震荡不会收敛。正常返状态返回自身的平均时间有限。对有限状态的马尔可夫链来说在不可约的条件下这条通常自动满足。但在实际业务里很少会先证明这些性质再建模。我的经验是先直接跑幂迭代如果发现结果不收敛或者跳出震荡再回头检查不可约性——通常是因为数据里有孤立状态或者吸收态导致不能形成闭环。一个典型的例子是用户路径分析中如果用户经常会“退出离开”那么“退出”成了一个吸收态一旦进入就回不来。这时候稳态分布可能会把绝大多数概率压在“退出态”上看起来像“所有人都流失了”这确实是这个模型告诉你的长期真相但对于想优化留存的产品而言你需要把“退出态”单独拎出来分析而不是混在稳态分布里一起解读。5. 真实业务建模三种数据形态下的转移矩阵处理5.1 用户路径分析从埋点日志到页面状态机用户路径分析是我遇到最多的场景。原始数据往往是设备ID、会话ID、页面名、时间戳这样的埋点表。要建转移矩阵最关键的步骤是“会话切分”。我一般把同一个用户两次操作间隔超过 30 分钟作为会话切割点然后对每个会话内的页面序列做状态映射。注意同一个页面反复刷新的情况要处理不然会构造出大量 self-loop把转移矩阵的统计搞得失真。我的习惯是同一个会话里同一个页面连续出现只保留一次连续刷新不算是有效跳转。页面状态一旦确定序列就变成了[首页, 搜索结果页, 商品详情页, 购物车, 支付页]然后直接传入build_transition_matrix。这个场景里稀疏性往往特别严重——几十个页面看似不多但跳转组合集中在少数高流量链路上大量矩阵元素是 0。业务上这种稀疏矩阵恰恰暴露了转化瓶颈比如发现“商品详情页”到“购物车”的概率很低就该去查落地页设计、价格展示、评论数量这类因素了。5.2 文本建模二元语言模型中的近邻词转移NLP 里最朴素的统计语言模型就是马尔可夫链的直接应用叫二元语言模型bigram model。状态是单词转移概率表示“当前词出现时下一个词是词库中某个词的概率”。构建方式几乎一模一样def build_word_bigram(text): words text.lower().split() vocab sorted(set(words)) return build_transition_matrix(words, vocab)这个模型的“智能计算”体现在生成过程给一个起始词根据转移矩阵采样下一个词再以这个词为条件采样下一个词直到生成结束。这样生成的句子虽然语义常常不太通顺但能很好地捕捉局部搭配习惯。这里有个绕不开的问题词表越大转移矩阵越稀疏大部分词对在训练语料里根本没出现过。所以语言模型常用加一平滑Laplace smoothing给每个可能的转移都加一个小概率smoothed (count_matrix 1) / (count_matrix.sum(axis1, keepdimsTrue) vocab_size)加了平滑之后模型不会再对没见过的二元组给出零概率生成和评估时才不会直接算崩。5.3 信用评级转移矩阵低频数据的平滑处理金融风控领域里的评级转移矩阵也是马尔可夫转移矩阵的典型应用。评级机构给债务人打一个等级比如 AAA、AA、A、BBB 以下等每年观察一次评级变化统计从一个等级迁移到另一个等级的频率得到的就是年度评级转移矩阵。这个场景跟前面有一个显著区别数据是低频年度数据样本量小很多评级状态之间的转移根本观测不到矩阵里大量 0。如果直接用频率估计模型会认为某些转移“永远不会发生”这在风控里是非常危险的结论。我的做法是引入业务先验把行业里长期积累的专家经验作为先验概率和观测频率做加权混合。用平滑技术比如 Dirichlet 先验把 0 概率替换成极小的正概率。对结果做敏感性分析让业务方知道概率区间而不是一个死板的点估计。这里要特别提醒一句评级转移矩阵这种高频抽象方法只能做参考不能替代专业的信用风险模型任何决策都需要配合业务底线和人工判断。6. 工程落地中躲不开的坑与我的应对习惯6.1 零行矩阵导致的除零与 NaN这是我在最开始做用户路径分析时栽过的第一个坑。某个用户页面序列只有一页比如“只打开了首页就退出”那么这个“首页”对外的转移次数是 0整个行加起来是 0。直接用count / row_sum就得到 NaN。经验总结任何状态下都必须有出边。处理方式有三种如果在业务语义上“页面被关闭”本身是一个状态那就把“退出/离开/关闭”补成一个显式的吸收态让“首页 - 退出”概率为 1。如果这个状态只是一个孤立噪声直接把它从状态空间里剔除。如果既不能剔除也不能补状态就用平滑方式给那一行填一个均匀分布至少保证矩阵合法性。我通常倾向于方案一和二因为他们有清晰业务含义方案三只作为兜底因为它会引入人为噪声。6.2 稀疏矩阵的存储与幂运算优化状态空间一旦到了几万维转移矩阵虽然稀疏但用 NumPy 的稠密二维数组存储会直接把内存吃爆。假设 10000 个状态稠密矩阵就是 10000×10000×8 字节 800MB这还没算运算时的临时变量。而实际上很多元素的非零占比不到 1%。遇到这种情况我建议切换到 SciPy 的稀疏矩阵from scipy.sparse import csr_matrix P_sparse csr_matrix(P_dense)在稀疏场景下幂迭代求稳态分布要比直接算P^n高效得多因为每轮迭代都是一次稀疏矩阵乘向量复杂度只跟非零元素数量有关而不是跟矩阵全尺寸有关。另外就算用稀疏矩阵matrix_power对高次幂也很容易把矩阵变稠密比如转移图的连通性高时几步之后几乎所有元素都可能非零。所以推演步数太长时我更推荐“直接循环迭代分布向量”而不是反复做矩阵幂。6.3 吸收态与不稳定收敛的判断吸收态问题在上文提过几次但要专门强调判断方法。一个状态 i 是吸收态当且仅当P[i, i] 1也就是从它出发只能留在自己。在一个非吸收系统中状态分布会向着“用户是不是都流失了”的结论收敛这个结论本身有价值但容易掩盖中间链路的问题。业务上如果关心转化漏斗应该把吸收态单独切出来计算“吸收概率”比如计算从“搜索页”进入后最终落到“流失态”的概率是多少。这个可以用吸收马尔可夫链里的基本矩阵来算但那是另一个深坑这里不展开。6.4 结果验证与模型诊断最后这点很容易被忽略转移矩阵是估计出来的不是天上掉下来的必须验证。我常用的验证手段有数据切分按时间把数据切成前 70% 和后 30%。用前 70% 构造转移矩阵在后 30% 上计算平均对数似然分数越高说明模型对真实数据解释力越强。频数对比用转移矩阵生成大量模拟序列然后对比真实序列中每个二元组的出现频次差异大的地方往往就是模型估计不准的地方。稳定性检验重新抽样或切片数据多次估计转移矩阵看看同一位置的转移概率是否稳定。如果波动过大说明数据量还不够或者状态设计不够合理。我自己吃过一次亏当时直接用全量数据构建转移矩阵看起来美滋滋但在时间维度的验证集上预测效果差得离谱。后来才发现某个页面在早期版本里没有上线前期的相对概率和后期的相对概率完全不同。所以建模之前一定要考虑时间段内的产品变化必要时做分段重新估计。做过几个项目下来我的总体感受是马尔可夫转移矩阵不是一个“金银铜”的复杂模型但它把随机过程、线性代数和工程实践衔接得非常巧妙。真正让它变得“智能”的不是模型本身而是我们用矩阵运算替代了路径枚举用线性代数工具包解决了状态推演又用工程手段处理了现实世界里的脏数据和稀疏问题。任何一个能把这几环串起来的人都能把这个经典模型用出花样来。
返回列表