资讯动态

强化学习基础核心:马尔可夫决策过程与Q学习实战

发布时间:2026/10/22 18:03:22 来源:尧图企业网站定制
聊到第13章我习惯先把前面十二章的思路停一下。前面的内容不管是线性回归、决策树、支持向量机还是各种神经网络结构本质上都在做同一件事从一批带标签或没标签的数据里学出一个静态规律然后用它去预测没见过的新样本。强化学习完全不在这个框架里。它的目标是让一个智能体在与环境不断交互的过程中学会一套决策策略在长期回报上拿到最好的结果。这一章讲的强化学习基础核心就是两个概念——马尔可夫决策过程MDP和Q学习它们是后面几乎所有强化学习算法的地基。这套基础能做什么先举一个最小例子。假设你有一只小机器人在一个棋盘格子上它不知道终点在哪只能试走到终点就加分走到坑里就减分。它没有老师只有一个环境给出的实时信号你得让它学会避坑并尽快到达终点。这一章学完后你能亲手写一个Q学习智能体让它跑几千个回合就学会接近最短的路线。对多数人来说这是强化学习的第一堂实验课也是理解价值函数、策略、时序差分这些进阶概念的必经之路。如果你是第一次接触强化学习或者之前学过基本机器学习和深度学习但对“智能体循环试错”的流程没有直观体感这一章可以直接按顺序看。如果你已经看过一些教程但没跑通代码我建议重点看第6节的完整实现和第7节的调参经验那是真正会花掉时间的地方。1. 第13章的岔路口从“看懂数据”切换到“学会做决定”先弄清楚我们到底换了什么赛道。前几章里训练集里的每个样本基本都有一个明确的监督信号分类有类别标签回归有目标数值模型学的是输入到输出的映射。强化学习的场景里通常没有人每时每刻告诉你“这一步动作对错”你只知道到达某一步之后收到一个数字——奖励而一个动作好不好要看它之后一串状态里累积下来的总收益。这就是“延迟回报”眼前这一步可能不赚甚至亏但整体能让后续吃到红利。举个例子下棋的时候你丢掉一个子从局部看是负回报但如果这个交换能换来之后的局面优势就是一个好决策。强化学习智能体要学的正是这种带前瞻性的判断。它不会像监督学习那样直接被告诉“这里应该下哪一步”而是通过反复尝试把每种操作的长期后果记下来最终形成一套策略。这套策略可以是一张查找表也可以是一个神经网络但底层都是同一个逻辑评估每个状态下每个行动的价值。这个转变看起来不大实际上一旦理解了之前很多“为什么模型要这样设计”的问题都会通顺起来。你不再纠结某个样本的预测误差而是关心整条轨迹上的累计奖励如何被最大化关心的对象也从“模型参数”变成了“策略”。这也是为什么马尔可夫决策过程这一套形式化框架如此重要——它给“和环境交互、从经验中学习”这件事提供了一个干净的数学底座。第13章适合两类人。一类是刚学完基础模型想知道机器学习还能解决什么“非标准问题”的学习者。另一类是已经看过一些强化学习名词Q值、奖励、回合、探索率但没理清它们之间关系的工程师。学完之后你应该能回答三个问题一个决策问题怎么写成MDPQ值为什么能指导动作选择以及Q学习算法在什么条件下能收敛。2. 马尔可夫决策过程拆解状态、动作、奖励、转移概率是怎么协作的马尔可夫决策过程其实是一个很朴素的建模框架。它把智能体和环境的交互抽象成一条不断循环的链路t时刻智能体处于状态S_t它选择一个动作A_t环境根据当前状态和动作给出奖励R_{t1}并转移到一个新状态S_{t1}接着进入t1时刻重复这个过程。这个循环写出来就是(S_t, A_t) → (R_{t1}, S_{t1}) → (A_{t1}) → … 简洁但足够描述绝大多数序贯决策问题。2.1 为什么说马尔可夫性质是“当前局面说了算”马尔可夫性质是整个框架中最关键也最容易被忽略的假设给定当前状态未来的状态转移和奖励分布与过去的历史无关。说人话就是——现在这一刻的信息已经包含了所有决定下一步所需要的知识你不需要记得三天前做过什么。用棋类来类比最直观。下棋时你只看当前棋盘局面就足以决定下一步因为局面本身就是过往所有落子的结果。你不会因为“当初第一步走的是哪个角落”就改变当前这一手的判断该看的是盘面本身。但如果换成一个机器人导航任务想判断它现在的坐标可能还需要知道它过去走过多长距离来估算累积噪声这时纯用当前坐标就不满足马尔可夫性质。实际工程里我们通常会通过重新设计状态空间把历史信息塞进当前状态里例如把“已经消耗的资源数”也作为一个状态维度来强行满足这个假设。这个性质之所以是基石是因为它把规划问题从“需要考虑整个历史轨迹”压缩成“只需要考虑当前节点”否则Q表和策略表根本没法建立——每个状态的价值还必须依赖来路维度会爆炸。学的时候抓住这一点后面看多智能体、部分可观测环境时就会明白它们的困难出在哪里。2.2 一个MDP的标准数学记号把一个MDP写成形式化定义通常包含四个元件状态集合S智能体可能处于的全部局面。动作集合A每个状态下可以选择的行动。转移概率P(s | s, a)从状态s执行动作a后落到s的概率。奖励函数R(s, a, s)环境在这个转移发生后给智能体的标量信号。再加一个策略π(a | s)表示在状态s下选择动作a的概率或确定性选择哪个动作。策略是我们要学习的东西而MDP是描述问题世界的参数。强化学习算法做的事情可以理解为在给定MDP定义下寻找一个最优策略π*使得从任何初始状态出发的期望累积回报最大化。我个人的习惯是每拿到一个实际问题先不要急着写代码而是先在纸上把四个元件列清楚。状态是离散网格还是连续坐标动作是一次性选择还是一个连续量奖励信号是稀疏出现的还是每一步都有写不清楚这三个问题后面算法调参会非常痛苦。尤其是奖励信号很多项目不是算法不行是奖励设计没有在MDP层面想清楚。2.3 用一个园区导航场景完整建模拿一个我实际用作课后练习的“园区无人配送小车”问题来演示。园区是一条6乘6的网格路小车从西南角出发要送到东北角的目标楼途中有些障碍物不能进入有些坑洼路段会有额外损耗。要素映射如下状态S小车当前所在的格子坐标例如(0,0)到(5,5)。动作A上、下、左、右四个方向。转移概率P如果前方不是障碍就以0.8概率成功移动到目标格0.2概率滑到旁边的垂直方向如果前方是边界或障碍则原地不动。奖励R到达目标楼获得10撞上障碍或落入坑洼获得-5普通移动每一步获得-0.1用于引导走出短路径。这样一个设定下智能体的目标就不是简单走到终点而是在“尽快到达”和“避免滑出问题路段”之间做权衡。你可以试着想象如果完全没有转移概率的随机性贪心地一直往右往上就能到终点加入滑移概率后靠近障碍物走就有风险距离惩罚又太狠也不行只能通过反复试错找到一个平衡路线。这正好是Q学习这类无模型算法最擅长的地方。3. 折扣因子gamma为什么未来的奖励要打折扣在MDP里智能体要最大化的是从某个时刻开始的累积回报。但这个累积回报不能简单把每一步奖励直接相加因为时间是无限长的直接求和会发散。所以我们引入折扣因子γgamma定义回报G_t为G_t R_{t1} γ R_{t2} γ² R_{t3} γ³ R_{t4} ...γ取值在0到1之间。γ越接近1智能体越看重远期收益γ越接近0它越急功近利。为什么需要折扣不止是数学上为了收敛更有经济学的类比现在的1块钱比一年后的1块钱更值钱因为可以拿去投资产生回报。同样在决策问题里奖励到来的时间越晚不确定性越大所以把未来收益按指数因子折算。3.1 折扣因子的实际含义有效时间视野如果你把折扣因子的含义翻译成“智能体计划多远的未来”可以这样近似γ的k次方缩到一半以下意味着k步之后的奖励对当前决策几乎没有影响。下面这张表是我常用的参考值γ取值大约有效视野适用场景0只看当前一步纯即时奖励任务类似瞬时反应0.5几步以内极短期的控制问题0.9十步左右大多数格子世界、简单游戏0.99几十步以上长期规划任务如围棋、机器人导航1无限视野有限步任务专用需要配合episode设计比如在第2节的园区配送例子里如果γ0小车只会选择“当前这一步奖励最大的动作”它走哪都是-0.1只有到终点那一步才10那么它永远看不见“只有走到终点才有大奖励”这个事实。γ0.9时它开始愿意忍受前面几步的小幅负收益换得后面较大的正收益。γ0.99时它会为更远处的目标规划出绕路方案即使前几十步没有明显回报。初学的时候最容易搞混的一点是折扣因子不是让智能体“短视”的坏东西而是控制它“愿意为远期目标牺牲多少眼前利益”的旋钮。任务越长γ一般要调得越接近1否则智能体根本不会为了30步之后的目标动作。反过来如果γ太大奖励信号传播完全依赖后面的奖励判断早期的Q值会非常不稳定学习会变慢。3.2 为什么不用平均回报而用折扣有人可能会问为什么不直接定义成“从当前开始的平均每步奖励”这样不就不用调γ了答案在于平均回报丢掉了对“时间偏好”的刻画。同样的总收益快速拿到和慢慢拖到很久后拿到在优化意义上应该有不同的价值。折扣形式自带了这个偏好而且它还有一个非常好的性质回报序列在任何状态下都是一个有限值这让贝尔曼方程和Q学习的收敛性有了保障。实际写代码时你不需要手动算无穷级数算法在一步步迭代中自然会把这些折扣累积进Q值里。关于γ的选择我的经验是不要一开始就纠结先用0.9跑通流程确认算法本身没问题后再根据最终策略的“眼光长度”决定是否调大。如果γ0.9下智能体能走到终点但路径绕远那就往0.99方向调如果它看起来完全不在乎中途撞障碍反而要调小。4. 贝尔曼方程与Q值把“未来的最优”压缩进当前价值的等式里有了MDP的定义和折扣回报下一步要解决的是怎么比较“哪个状态更好”。状态价值函数V_π(s)定义为从状态s出发按照策略π持续行动期望的折扣回报。值越大代表这个状态越有利。类似地动作价值函数Q_π(s, a)定义为从状态s出发先执行动作a之后按策略π行动期望的折扣回报。V函数是对状态的抽象评价Q函数是对“状态-动作对”的具体评价。4.1 从V函数到Q函数的递推关系这两个函数之间存在一个漂亮的递推关系它其实是从回报的定义直接变形得到的V_π(s) Σ_a π(a|s) Σ_s P(s|s,a) [ R(s,a,s) γ V_π(s) ]对应的Q函数版本Q_π(s, a) Σ_s P(s|s,a) [ R(s,a,s) γ Σ_a π(a|s) Q_π(s, a) ]这个式子翻译成大白话就是当前状态的价值 执行动作后的即时奖励 折扣后的下一个状态价值。它不是一个新的理论而是“G_t定义里前后两项关系”的直接展开。之所以重要是因为它把一个需要看完整条轨迹才能估计的量变成了一个只依赖相邻两项的局部等式。4.2 为什么贝尔曼最优方程能成为“自洽解”当策略是最优策略π*时V和Q满足的是最优贝尔曼方程。对最优Q函数来说有一个更简洁的形式Q*(s, a) Σ_s P(s|s,a) [ R(s,a,s) γ max_a Q*(s, a) ]核心变化是下一步的动作不再参考某个固定策略分布而直接取能达到的最大值。这个方程的意义在于如果我知道了所有状态下所有动作的最优Q值那么决策就变成了一件特别简单的事——每步都选Q*(s, a)最大的动作因为Q值已经把未来的最优结果折现回当前状态了。这个式子的“自洽性”很值得停下来体会。左边是未知的Q*(s, a)右边也依赖Q*只不过依赖的是下一个状态的值。这种递归结构意味着哪怕没有全局奖励地图只要不断用右边去更新左边等式最终能够自己“咬合”起来让两边相等。这正是Q学习能在没有环境模型的情况下工作的理论根基我们不需要完整转移概率P(s|s,a)只需要不断和真实环境交互从抽样中近似那个期望值。在这个阶段我建议不要太深钻收敛性证明先把“最优Q值隐含了未来最优决策”这句话反复咀嚼。后面看到DQN、Actor-Critic等算法时你会发现它们做的还是同一件事——用各种手段逼近一个满足贝尔曼方程的函数。5. Q学习的更新公式和时间差分思想没转移概率也能学有了理论铺垫Q学习算法的更新公式就非常自然了Q(s, a) ← Q(s, a) α [ r γ max_a Q(s, a) - Q(s, a) ]这里的α是学习率r是执行动作a后真实收到的奖励s是实际转移到的状态。整个更新过程可以拆成三块原来的估计、时序差分目标TD target、时序差分误差。5.1 TD target 与“用现实修正估计”先看带方括号的式子r γ max_a Q(s, a)。这个值叫TD target它是由两部分拼成的一部分是刚拿到的真实奖励r另一部分是对未来收益的现有估计 γ max_a Q(s, a)。两者相加代表“现实反馈 自身猜想”的组合比原来单纯依靠某个初始随机估计要可靠得多。而整个方括号项就是TD误差它刻画的是当前估计与这个更合理期望之间的差距。这个过程很像一个人在没有地图的情况下找路一开始脑子里有个模糊的地图每走一步根据实际看到的街道情况修正对下一区域的估计。你走的越多地图越精确。这也是“时序差分”名字的由来——我们不是等一整条轨迹结束后再回看所有奖励而是每走一步就对比一次预测和现实把差距增量式地回传给前一步的状态价值。5.2 更新公式逐项解释我把每一项再展开说一下Q(s, a)更新前对这个状态动作对的估计。r真实奖励来自环境的唯一反馈它可能是正、负或零是更新里最不可替代的信息。γ max_a Q(s, a)未来收益的估计用“下一个状态里最大Q值”来代表最优后续策略的价值。注意这里的max是站在“理想决策者”的视角不管行为策略实际上怎么选择。α学习率控制本次更新愿意改变多少旧估计。α0意味着完全不见新信息α1则直接把旧值替换成新的TD target。如果认为可信度排序是“真实奖励 对整个未来的现有估计”就能理解为什么要用TD target做底。至于为什么不直接把Q(s,a)更新成r γ max Q(s,a)而是保留旧值按比例融合原因是单次采样存在巨大噪声一步到位会让Q值剧烈抖动收敛性质很差。缓慢融合相当于做指数平均让Q表逐渐稳定下来。5.3 为什么Q学习属于离策略算法Q学习里有一个非常容易萌混过关的细节行为策略用来产生实际动作和目标策略用来评估最优动作不是同一个。行为策略通常用ε-greedy以ε概率随机探索以1-ε概率选当前Q值最大的动作而更新公式里的max_a Q(s,a)默认了“下一步一定选最优动作”两者是分离的。这正是离策略off-policy的含义。优点是智能体即使在探索过程中走了歪路学到的东西仍然能反映最优策略的价值缺点是不像Sarsa那样会考虑“行为策略本身的随机性”对实际收益的影响。这个区别在涉及安全性或需要稳健控制的任务里尤其重要。Sarsa作为同策略on-policy版本后面进阶章节会展开这里只要知道两个算法的差别根源就是更新公式里有没有那个对后续策略取最大值的操作。顺带说一个我踩过的坑如果ε一直保持一个很大的值Q学习学到的最优Q值仍然是偏乐观的因为它总是用max估计未来而行为策略本身还在大量随机乱走。这会导致一个现象Q表训练结束后一旦把ε降到0去实际执行平均回报反而下降因为Q值是在高探索率环境下“校准”的测试时的状态访问分布变了。要规避这一点要么训练后期强制让ε衰减到很小要么把目标策略的估计也换成带探索修正的形式。6. 手写一个Q学习智能体从一个小网格环境跑到收敛现在到了动手的地方。我准备了一个最简单的网格世界环境5乘5格子有起点、终点和一个陷阱动作只有上下左右。完全不依赖深度强化学习框架只用Python和numpy就能跑通。这一节请一定自己敲一遍强化学习不是看会的是跑会的。6.1 环境怎么定义网格按坐标(行, 列)编号起点是(0,0)终点是(4,4)陷阱在(2,2)。普通每一步奖励-0.1到达终点10掉进陷阱-10。动作0、1、2、3分别代表上、下、左、右。如果动作会撞出边界留在原地并仍然获得-0.1的移动惩罚。这里引入了一点随机性每次移动有0.8概率按指定方向走0.2概率滑到左右方向。这样可以模拟真实环境的不可控因素也更容易看出Q学习的价值。如果你第一次跑也可以把随机性去掉算法会在更少回合内收敛效果更直观。6.2 完整代码训练、评估、提取策略import numpy as np import random class GridWorld: def __init__(self, size5): self.size size self.start (0, 0) self.goal (4, 4) self.trap (2, 2) self.actions [(-1, 0), (1, 0), (0, -1), (0, 1)] # 上、下、左、右 self.reset() def reset(self): self.state self.start return self.state def step(self, action_idx): r, c self.state dr, dc self.actions[action_idx] # 以0.8概率按预定方向走0.2概率横向滑移 if random.random() 0.8: nr, nc r dr, c dc else: if random.random() 0.5: nr, nc r dc, c dr # 垂直方向发生滑移 else: nr, nc r - dc, c - dr # 边界检查不能走出网格 if 0 nr self.size and 0 nc self.size: self.state (nr, nc) else: self.state (r, c) if self.state self.goal: reward 10.0 done True elif self.state self.trap: reward -10.0 done True else: reward -0.1 done False return self.state, reward, done def train_q_learning(env, episodes20000, alpha0.1, gamma0.95, epsilon0.8, epsilon_min0.01, decay0.9995): # Q表用字典state - 长度为4的数组 q_table {} def get_q(s): if s not in q_table: q_table[s] np.zeros(4) return q_table[s] total_rewards [] for ep in range(episodes): s env.reset() done False total 0.0 while not done: q get_q(s) # epsilon-greedy 动作选择 if random.random() epsilon: a random.randint(0, 3) else: a int(np.argmax(q)) s_next, reward, done env.step(a) q_next get_q(s_next) # Q学习更新公式 td_target reward gamma * np.max(q_next) * (0 if done else 1) td_error td_target - q[a] q[a] alpha * td_error s s_next total reward total_rewards.append(total) epsilon max(epsilon_min, epsilon * decay) if (ep 1) % 2000 0: print(fEpisode {ep1}, avg_total_last_2000{np.mean(total_rewards[-2000:]):.2f}, epsilon{epsilon:.3f}) return q_table env GridWorld() q_table train_q_learning(env) # 提取最终策略每个状态选择Q值最大的动作 action_names [上, 下, 左, 右] print(\n最终策略) for r in range(5): row [] for c in range(5): s (r, c) if s env.goal: row.append(终) elif s env.trap: row.append(陷) else: q q_table.get(s) if q is None: row.append(?) else: row.append(action_names[int(np.argmax(q))]) print( .join(f{x:3} for x in row))这段代码大约90行没有用到任何额外强化学习库逻辑清晰。特别强调一下更新公式里的那个(0 if done else 1)当一步到达终止状态后后面已经没有未来奖励所以折扣未来项必须乘0。很多初跑代码的同学忘了这个细节导致终点附近的Q值被重复抬高策略却收敛得又慢又怪。6.3 跑完以后看什么正常跑完2万回合你会看到两种情况。一是控制台输出的最近2000回合平均回报从负数缓慢爬升最后稳定在9左右说明智能体大部分回合都能成功到达终点。二是策略表里从起点到终点的路径上每个格子都指向正确的下一步方向。如果你用了带滑移概率的环境你还会发现靠近陷阱边缘的格子策略变成了“绕开”而不是直线莽过这就是Q值正确吸收了风险信号的结果。我建议你接下来做三个小实验。第一把epsilon衰减去掉固定成0.2看看最终策略和收敛速度有什么变化。第二把gamma从0.95改成0.5观察路径会不会变短视。第三把普通移动奖励从-0.1改成0看看智能体是否还会倾向于走最短距离。这些实验比看十篇文章都更能建立直觉因为你会亲自感受到同一个更新公式在不同超参数下如何输出截然不同的行为。7. 训练基础设施级别的经验探索率、学习率与奖励设计的常见坑代码跑通只完成了一半。真正让人头疼的往往不是理解Q学习更新公式而是在一个具体任务里让它稳定收敛。我把实际带学员做类似实验时最常遇到的四个坑整理一下这些经验不只适用于这个网格世界也适用于后续所有强化学习项目。7.1 探索率衰减方案不衰减一定出问题衰减太快也不行epsilon-greedy是Q学习最基础的动作选择策略。它的作用是在初期保证智能体有足够概率尝试“看起来不是最优”的动作从而发现全局最优路径。问题在于epsilon如果一直很大比如0.5那么即使训练后期智能体已经知道最佳路线它还是有相当大的概率去走弯路平均回报就上不去。反之如果epsilon衰减过快从0.9在几千回合内直接跌到0.01初期还没探索完环境智能体就过早开始利用局部最优最终策略可能画在“看起来很合理但明显不是全局最优”的路线上。我的习惯做法是设置一个下限让epsilon在训练尾声仍保留一个极小值例如0.01而不是衰减到0。这样既保证最终策略稳定又保留极小概率的补救探索。衰减速度则需要根据任务步数和环境复杂度来试一个粗略设计是希望智能体在大约一半的训练回合内完成主导探索剩下的回合逐步把epsilon从高位压到下限。不要一刀切地固定公式多跑几组对比输出平均回报曲线比任何理论指导都直接。7.2 学习率alpha过大抖动不收敛过小又太磨蹭Q学习的更新是增量式的学习率alpha决定了每次TD误差有多大的比重被写回Q表。alpha调太大会让Q表来回震荡一个回合学到终点路径下一个回合又因为几个失败样本把路径上的Q值打得乱七八糟。alpha调太小则会让Q值像蜗牛一样爬几万回合都不一定能把奖励信号传播到起点附近。我在网格环境里的经验是alpha0.1通常是个安全的起点如果状态空间较大或奖励尺度很大可以适当调小到0.05。另有一个实用技巧训练后半段适当降低alpha类似学习率退火可以帮Q表更精细地收敛。很多开源实现里不统一调度alpha但也跑得动只是在复杂任务里固定alpha往往需要额外增加很多回合才能达到同等效果。7.3 奖励设计的门道稀疏、正负不对称与奖励塑造奖励设计是强化学习里最重要也最玄学的部分。最直接的做法是“到了终点1其他都是0”这是纯稀疏奖励。在这种设置下智能体前几千回合基本上全靠随机探索恰巧踩到终点才能产生一次有效学习起点附近的状态Q值几乎一直不动训练非常慢。常见解法是给每一步加一个小的负奖励比如-0.1引导智能体朝“少走步数”的方向优化。但这里有个明显的陷阱如果负奖励绝对值过大它会压倒终点奖励导致智能体宁愿停在原地也不敢乱走如果过小它又可能不在意绕路。更隐蔽的问题是奖励信号的正负不对称性把踩陷阱设置成-10和设置成-0.5会显著改变智能体对“陷阱附近”状态的看法进而改变路径选择的风险偏好。我用过不少奖励塑造手段核心原则是确保任何中间奖励都不会让智能体发展出“为了刷小分而忽略主目标”的行为。例如如果每一步都根据“离终点更近就加分”智能体就可能在一个局部瓶颈区域来回蹭看起来每步收益都不错最终任务却完不成。7.4 从Q表到DQN状态空间一大查表就开始失效最后补一个承上启下的提示。第6节的网格世界只有25个格子每个格子四种动作用一张字典写的Q表就完全够用。但真实任务的状态往往远不止这么少围棋有约10的170次方个状态不可能给每个状态都存一张Q表。Q表失效后的自然替代是用一个函数近似器去拟合Q(s, a)最典型的就是把Q表换成神经网络输入状态、输出各动作的Q值这也就是DQN的雏形。一旦你接受了这个想法深度强化学习的整个路线就从“查表”变成了“用网络拟合表格”仍然用同一个TD更新公式构造目标值只是每次更新的对象从表格单元格变成了网络权重。值得注意的是神经网络引入之后会出现灾难性遗忘、样本相关性、目标不稳定等问题于是重放缓冲区、目标网络、双Q等等技巧相继而来。你如果现在把Q学习更新公式吃得足够透后面所有技巧都是在解决“把这个更新放到神经网络上会出什么问题”不会觉得突兀。我自己在做这个系列实验时最大的感受是Q学习最容易被低估的地方是它明明只靠一个更新公式就能让一个完全不认识地图的智能体逐渐在脑海里长出一张价值地图。这种“从无模型的交互中提取结构化决策”的过程是强化学习和其他机器学习范式最迷人的差异。如果你第一次跑通网格世界并看到策略表越来越合理建议停下来多观察几组不同超参数下的策略差异那比赶进度进入下一个高级算法有用得多。真正影响你后面调试效率的往往就是现在这些最基础、最朴素的经验。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑