简介:这份PPT面向机器学习入门者与人工智能专业低年级学生,聚焦第一章机器学习概述,帮助读者建立课程整体框架,理清“为什么学、是什么、怎样学”三个核心问题。内容从人工智能定义与三次浪潮切入,串联自动推理、专家系统到深度学习的发展脉络,并梳理机器学习在人工智能研究领域与课程体系中的位置,说明高数、线代、概率论及编程等先修基础。资源包共1个pptx文件,约10.28MB,结构紧凑,适合课堂讲授、自学预习或复习提纲使用。讲解同时落到机器学习的核心对象数据、模型与优化,区分离散与连续数据、条件概率分布与决策函数等概念,便于读者形成从数据到模型再到优化的基础认知。目前已有165人学习,可作为入门阶段快速建立知识地图的参考材料。
1. 三次浪潮的暗线:机器学习的第一课为什么先讲「它怎么失败」
机器学习这门课真正的分水岭,不在你记住了多少算法名,而在你能不能看懂它过去两次是怎么失败的。多数入门讲义按时间线罗列成果,读起来像荣誉榜;这份资料的取舍恰好相反,它把「为什么会失败」摆在「能做什么」前面。原因很实际:第一次浪潮把智能等价于逻辑推理,第二次浪潮把智能等价于专家手写的知识库,两次都碰到了同一个问题——知识从哪来。当知识必须由人一条条写进去,系统就永远追不上现实世界的组合爆炸。第三次浪潮改变的不是算力,而是这个前提:让机器自己从数据里拟合规律。想通这条线,「数据 + 模型 + 优化」就不再是三句口号,而是对前两次失败的直接回应。
1.1 一个等式其实是三个可替换的接口
资料里那句「机器学习 = 数据 + 模型 + 优化」,新手容易当成记忆口诀划过去。把它当成工程接口看,含义就完全不同:数据决定了输入空间的形状,模型声明了你要搜索的那一族函数,优化规定了在这族函数里挑一个的标准。三者是乘法关系,任何一层塌了,整个系统就是零。举个最常见的组合:数据全是离散 ID、模型选了基于欧氏距离的分类器、优化用均方误差,三件事单看都没错,拼在一起却必然跑不出结果。后面所有章节,本质都在拆这三层各自在管什么。
1.2 为什么「数据」被放在第一位
传统程序里,数据和逻辑是分开的:人写规则,数据只是输入。机器学习把这个关系倒过来——规则本身要从数据里估出来。这意味着数据不再是被动的原料,而是承担了「知识载体」的角色,它的分布、噪声、标注质量,直接决定了模型能学到什么上限。这也是第三次浪潮和前两次最本质的差别:知识不再由人编码,而是从样本里统计出来。理解了这一层,你才会明白为什么预处理、采样、标注一致性这些「脏活」在真实项目里占掉大半时间,而不是模型调参。
1.3 这门课的组织方式透露了什么
从「为什么要学」到「什么是机器学习」再到「怎么学」,三段式结构看着像教学模板,实际上对应三个不同层次的问题:动机层回答这门学科解决了什么别人解决不了的问题,定义层划定研究对象和边界,方法层给出可执行的路径。一份概述性质的材料如果只讲定义,学完你只知道名词;把动机放在最前面,你才有可能在遇到新算法时自己判断它填补了哪块空白。下面几章就按这个顺序往下拆。
2. 「数据 + 模型 + 优化」:一个等式拆开后的三层抽象
把这三层拆开看,会发现每一层都有独立的失效模式和独立的排查手段。工程上最怕的不是某一层弱,而是把三层的责任混在一起——模型效果不好时不知道该改数据、换模型还是调优化器,只能靠试。分层之后,问题就变成了可定位的:先确认数据里有没有可学的信号,再确认模型的假设空间装不装得下这个信号,最后才轮到优化能不能找到它。这个顺序不能反,反了就是典型的瞎调参。
2.1 数据层:为什么离散数据和连续数据要走不同的算法
数据进入模型前必须先变成向量,但「变成向量」这一步的代价经常被低估。连续特征(温度、价格、像素值)天然带序关系,两点之间的距离有意义;离散特征(城市编号、商品类目)不带序关系,把它当数字喂进去,模型会误以为编号 3 的类目「比编号 1 更接近编号 2」。常见的处理是独热编码或嵌入,把每个类别映射到一个独立的维度上,代价是维度膨胀和稀疏性。
量纲问题更隐蔽。两列特征,一列是金额(千级),一列是比率(零点几),直接送进基于距离的模型,第一列的差值会压过第二列两三个数量级,效果等同于模型只看到了第一列。标准化解决的就是这件事:
import numpy as np # 两列特征:第一列量纲在 10^3,第二列在 0~1 X = np.array([[1200.0, 0.12], [800.0, 0.35], [1500.0, 0.08]]) mu = X.mean(axis=0) # 逐列均值,形状 (2,) sigma = X.std(axis=0) # 逐列标准差,形状 (2,) X_std = (X - mu) / sigma # 每列变成均值 0、方差 1,量纲被抹平 print(X_std.round(3))这段代码的作用是把每一列各自平移缩放到同一尺度,axis=0表示沿样本方向统计,得到的是「每个特征一个统计量」。这里有个容易翻车的细节:均值和标准差只能用训练集算,再套用到验证集和测试集上。如果用全量数据算统计量,测试集的信息就顺着这两个标量泄漏进了训练过程,线下指标会虚高,上线后掉点,而且极难定位。
按模型族区分,量纲敏感度差别很大。基于距离或内积的模型(近邻、支持向量机、神经网络、聚类)对尺度高度敏感,必须标准化;基于分裂的树模型只关心特征的相对顺序,对单调变换不敏感,量纲影响小。反过来,树模型对特征单调变换免疫这一点,也意味着它抓不住特征之间的线性组合关系,需要人工构造交叉特征。这就是「离散配什么、连续配什么」背后的实际考量,不是口味问题。
还有一类数据在概述里常被一笔带过:文本、图像、音频。它们原始形态是高维非结构化信号,需要先转成数值表示才能进入同一套框架。这一步在早期靠人工设计特征,在深度学习时代交给模型自己学,但「把原始信号变成向量」这件事本身从未消失,只是从人的工作变成了网络前几层的职责。
2.2 模型层:假设空间——你能学到什么,早在选模型时就定了
模型不是一个算法名字,而是一族函数的集合,术语叫假设空间。线性模型对应的是所有形如w·x + b的函数,决策树对应的是所有能用有限次轴对齐切分表达的分段常数函数,神经网络对应的是多层线性变换与非线性激活的复合。选模型,本质就是事先声明「我认为这个问题的规律长什么样」。
这个声明带来的约束叫归纳偏置。卷积网络假设局部相关性和平移不变性,所以在图像上样本效率极高,换成表格数据就未必比梯度提升树好;近邻方法假设「特征空间里距离近的样本标签也近」,遇到高维稀疏数据时这个假设直接失效,因为高维下几乎所有点对的距离都趋同。没有哪个模型族是普适的,这就是「没有免费午餐」的实际含义——不是玄学,是假设空间大小的必然结果。
| 模型族 | 假设空间形态 | 归纳偏置 | 典型适用场景 |
|---|---|---|---|
| 线性模型 | 超平面划分 | 特征与输出近似线性可加 | 高维稀疏、需要可解释权重 |
| 决策树 / 集成树 | 轴对齐分段常数 | 特征间交互以分裂形式出现 | 表格数据、混合类型特征 |
| 近邻方法 | 由样本点隐式定义 | 局部邻域标签一致 | 低维、样本量适中 |
| 神经网络 | 多层复合函数 | 层次化、可微、局部性 | 图像、语音、文本等高维信号 |
表的最后一列不是推荐清单,而是「归纳偏置和数据形态匹配」的结果。挑模型时先问自己:这个问题的规律更接近线性可加、分段切分,还是层次组合?答案比排行榜靠谱。
模型容量是另一个维度。容量指假设空间能表达的函数有多复杂,容量太小连训练集都拟合不了,叫欠拟合;容量太大则会把训练集里的噪声也当成规律记住,叫过拟合。这里有个反直觉的点:容量不是越大越好也不是越小越好,它要和样本量、噪声水平匹配。同一份数据上,参数更多的模型反而可能泛化更好,前提是有足够的样本和合适的正则约束。
2.3 优化层:损失函数和「最优模型」到底是什么关系
有了假设空间,还需要一个标准来判断哪个函数「好」。这个标准就是损失函数,它把「预测偏离真实标签多少」量化成一个标量。训练的目标是让训练集上的平均损失最小,这个量叫经验风险。注意它是「经验」的——只是在手头这批样本上的平均,不等于真实分布上的期望风险。两者之间的差距,就是泛化误差的来源,后面讲评估和验证集,本质上都是在估计这个差距。
优化过程通常是迭代的:从一组初始参数出发,算损失对参数的梯度,沿负梯度方向走一小步,重复。梯度方向是损失下降最快的方向,步长由学习率控制:
import numpy as np # 一维凸函数 f(w) = (w - 3)^2,梯度为 2(w - 3),最优点在 w = 3 w = 0.0 lr = 0.1 for i in range(30): grad = 2 * (w - 3) # 损失对参数的偏导 w -= lr * grad # 沿负梯度方向更新参数 print(round(w, 4)) # 输出接近 3.0lr的取值直接决定行为:太大时每步跨过谷底,损失震荡甚至发散;太小时收敛极慢,训练几十轮看起来毫无进展。实践中这两个现象很容易被误判成「模型不行」,其实只是步长没调好。更麻烦的是,学习率和其他超参存在耦合,改了网络结构往往要重新扫一遍。
深度学习里还会遇到非凸损失,梯度下降只能保证收敛到局部极小或鞍点附近。但这在实践中没那么致命:高维空间里真正的局部极小很少,大量鞍点的损失值又接近全局最优,加上随机梯度带来的噪声本身有逃离鞍点的作用。真正影响结果的是初始化和数据顺序,而不是「有没有找到全局最优」。
3. 异或问题:一条直线画不出来的几何后果
异或是一个只有四个样本的小问题,输出规则是「两个输入不同则为 1,相同则为 0」。把它画在二维平面上,两个正例在对角,两个负例在另一条对角,任何一条直线都无法把两类分开。这个几何事实看起来平平无奇,却是第一次浪潮转向低谷的技术原因之一:当时主流的单层感知机只能表达线性决策面,异或直接超出了它的假设空间。问题不在于训练不够久,也不在于数据不够多,而在于模型族里根本不存在解。
3.1 感知机的能力边界:线性可分到底限制了什么
感知机的形式极其简单:对输入做加权求和加偏置,过阈值得到 0 或 1。它的决策面是w·x + b = 0,在二维是一条直线,在高维是一个超平面。学习规则也很直白:分对了不动,分错了就把权重往减少错误的方向推一点。
import numpy as np # 异或的四个样本:输入二维,标签 0/1 X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float) y = np.array([0, 1, 1, 0], dtype=float) w = np.zeros(2) # 权重,对应两个输入维度 b = 0.0 # 偏置,控制决策面到原点的距离 for step in range(200): err = 0 for xi, yi in zip(X, y): pred = 1.0 if xi @ w + b > 0 else 0.0 # 线性组合 + 阶跃 delta = yi - pred if delta != 0: w += 0.1 * delta * xi # 只在分错时更新 b += 0.1 * delta err += 1 if err == 0: print("第 %d 轮收敛" % step) break else: print("200 轮内未收敛") # 实际输出:200 轮内未收敛关键在于循环里的if delta != 0:更新只在分错时发生。由于不存在任何一组(w, b)能把这四个点全部分对,每一轮必然存在错分样本,权重被反复推来推去,err永远降不到 0,训练过程表现为权重震荡而非收敛。这段代码证明的不是「训练轮数不够」,而是假设空间里没有解——多加几万轮也一样。
感知机收敛定理只在线性可分的前提下成立:如果数据确实能被某个超平面分开,这个规则会在有限步内找到一组解。异或恰好落在这个前提之外,所以它成了检验「模型能力边界」的标准例子。
3.2 隐藏层:为什么「多一层」就能把直线掰弯
要解决异或,需要在输入和输出之间插入一层,先把原始空间做一次非线性变换,再在变换后的空间里做线性划分。直观理解:单个神经元切一刀,两个神经元各切一刀,两刀组合出的区域就能把对角线上的两类分开。
import numpy as np X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float) # 隐层两个 ReLU 单元,各自切一刀 W1 = np.array([[1, 1], [1, 1]], dtype=float) # 两个神经元都取 x1 + x2 b1 = np.array([0.0, -1.0]) # 第二个神经元阈值上移 1 W2 = np.array([[1.0], [-2.0]]) # 输出层组合:h1 - 2*h2 b2 = np.array([0.0]) h = np.maximum(0, X @ W1 + b1) # ReLU:max(0, z) out = (h @ W2 + b2 > 0.5).astype(float) # 阈值 0.5 得到 0/1 print(h) print(out.ravel()) # 输出 [0. 1. 1. 0.]逐行看:W1让两个隐层单元都接收x1 + x2,区别在偏置——第一个单元阈值 0,第二个阈值 1。于是第一个单元在「至少有一个输入为 1」时激活,第二个单元只在「两个输入都为 1」时激活。输出层做h1 - 2*h2,就得到「恰好一个为 1 时为正、其余为 0 或负」的结果,正是异或的真值表。
这里真正起作用的是 ReLU 引入的非线性。如果去掉激活函数,两层线性变换复合起来仍然是一个线性变换,决策面照样是直线,加多少层都没用。所以「多一层」的准确说法是「多一层带非线性的变换」,非线性才是把直线掰弯的原因。
把上面的过程推广,就得到通用近似性质的直观版本:足够多的分段线性单元可以逼近紧集上的连续函数。但这句话只保证「存在」,不保证「训练能找到」,也不保证样本量够。理论上的表达能力和实际可学性之间隔着优化和泛化两道坎,这是很多初学者最容易混淆的地方。
3.3 反向传播:梯度是怎么一层层传回去的
多个层叠起来之后,输出层的误差要怎么告诉隐层?方法是链式法则。把网络看成计算图,每个节点是一个可微运算,损失对某个参数的梯度等于「损失对该节点输出的梯度」乘以「该节点输出对参数的梯度」,一层层往前乘。
import numpy as np # 单样本:x -> 隐层(ReLU) -> 标量输出,演示梯度回传 x = np.array([1.0, 1.0]) W1 = np.array([[1.0, 1.0], [1.0, 1.0]]) W2 = np.array([1.0, -2.0]) z1 = W1 @ x # 隐层净输入 h = np.maximum(0, z1) # ReLU 激活 o = W2 @ h # 输出 loss = 0.5 * (o - 1.0) ** 2 # 平方损失,假设目标值为 1 dL_do = (o - 1.0) # 损失对输出的偏导 dL_dh = dL_do * W2 # 输出层把误差按权重分摊回隐层 dL_dz1 = dL_dh * (z1 > 0) # ReLU 导数:正区间为 1,非正区间为 0 dL_dW1 = np.outer(dL_dz1, x) # 隐层权重梯度 = 上游梯度 × 本地输入 print(dL_dW1)两个细节值得盯住。第一,dL_dh = dL_do * W2这一步说明误差是「按连接权重分摊」回去的,权重大的连接承担更多责任,这解释了为什么初始化时权重不能全设成同一个值——否则所有隐层单元收到完全相同的梯度,永远学不出差异。第二,(z1 > 0)是 ReLU 的导数,当某个单元的净输入长期为负,它的梯度恒为 0,参数再也不会更新,现象上表现为这个单元彻底失效。学习率过大导致参数被推到大负区间,就是常见的成因之一。
4. 从写规则到学表示:三次范式迁移到底换掉了什么
把三次浪潮放在一起看,会发现它们换掉的不是算法,而是「知识从哪里来」这个前提。推理期认为知识是逻辑规则,知识期认为知识是专家写下的条目,学习期认为知识藏在数据分布里、由模型拟合出来。前两次的共同假设是「人可以把自己的知识完整表述出来」,而这个假设在开放环境里几乎必然崩塌——规则和条目永远列不完,而且会互相冲突。
4.1 推理期与知识期:为什么「把知识写下来」这条路会崩
推理期的思路很朴素:给机器装上逻辑,它就能推理出结论。上世紀五十年代中期那场夏季研讨会之后,一批自动定理证明程序确实证明了教科书里的定理,跳棋程序也能自己改进棋力。但这条路很快撞上两堵墙:一是组合爆炸,搜索空间随问题规模指数增长,当时的机器算不动;二是常识问题,形式化系统处理不了「杯子掉地上会碎」这类人眼都不用想的知识。
知识期的应对是把专家经验编码成规则库,配合推理引擎做诊断、配置这类垂直任务。这条路在特定领域确实跑通过,一度让产业界大量投入。但成本结构很快暴露:规则要一条条访谈专家、手工录入,维护成本极高;知识库膨胀之后,规则之间开始冲突,加一条新规则可能破坏原有的推理链;系统在边界情况外完全失能,不会「差不多」。这些问题的共同根源是知识由人手工编码,系统规模和一致性无法同时保证。
用一个具体例子能看清这种脆弱:机器翻译早期走的是规则加词典的路线,把一句话按语法结构拆开、逐词替换、再按目标语言语法重组。结构稍复杂或带习语,输出就不可读;而且每发现一类错误,就要补一批规则,规则之间又互相干扰。这不是工程能力问题,是范式上限。
4.2 学习期:四件事同时到位才叫拐点
第三次浪潮常被归结为「深度学习起来了」,但真正让拐点成立的是四件事同时到位,缺一件都跑不通。
| 条件 | 缺了会怎样 | 具体表现 |
|---|---|---|
| 算法演进 | 无法训练深层网络 | 层数一多梯度消失或爆炸,深层退化 |
| 理论基础 | 不知道为何有效、何时失效 | 只能靠试错,无法预判失败模式 |
| 数据支撑 | 参数学不出来 | 参数远多于样本,严重过拟合 |
| 算力提升 | 训练时间不可接受 | 实验周期以周计,迭代速度归零 |
这张表的价值在于解释「为什么是那个时候」。反向传播的思路很早就有,但直到算力足以支撑大规模矩阵运算、数据规模足以约束参数量,它才从理论变成生产力。反过来说,如果你在资源受限的场景下硬套大模型方案,本质上是在重复一个前提不成立的选择。
4.3 特征工程与表示学习:人找特征和机器找特征的分界线
传统机器学习流程里,特征工程占掉大半工作量:从原始数据里手工构造统计量、交叉项、窗口聚合,再喂给分类器。这实际上是人在替模型承担「找表示」的工作,模型的假设空间被人的先验知识大幅收窄,好处是样本效率高,坏处是人的想象力成了上限。
表示学习把这一步交给网络的前几层。卷积核、词嵌入、注意力权重,本质上都是可学习的特征提取器,训练过程同时优化表示和决策边界。代价是样本需求大幅上升,因为模型要自己搜出人本来直接给它的东西。这也解释了一个常见困惑:小样本表格任务上,手工特征加树模型常常打赢深度网络,不是网络弱,是它还没学够。
5. 常见问题与排查:入门阶段最容易翻车的五个现象
这一章把入门阶段最常遇到的五类问题整理成「现象 → 原因 → 解决」的形式。它们的共同点是:报错信息不会出现,代码能跑通,指标也打印出来了,但结论是错的。这类问题靠看日志发现不了,只能靠建立正确的检查顺序。
5.1 现象一:训练集准确率很高,测试集一塌糊涂
现象:模型在训练集上准确率 0.98,换到测试集掉到 0.62,损失曲线在训练集上一直降。
原因:过拟合,模型记住了训练集的噪声而不是规律;也可能根本不是过拟合,而是训练集和测试集分布不一致,比如测试集里的类别在训练集里没出现过。还有一种隐蔽情况:预处理时用了全量数据统计量,测试集信息泄漏进了训练。
解决:先做三件事——确认两个集合的构造方式一致、确认所有统计量只在训练集上拟合、再画学习曲线看训练和验证损失的差距。差距大是过拟合,加正则、减容量、增数据;两边都差是欠拟合,方向完全相反。
5.2 现象二:损失不下降,或者剧烈震荡
现象:训练几十轮,损失在一个值附近来回跳,或者直接变成 nan。
原因:学习率过大是最常见的,每步跨过谷底;也可能是梯度爆炸,深层网络里梯度连乘后数值溢出;输入特征量纲差异过大,导致不同方向上的曲率悬殊,单一学习率无法兼顾。
解决:先把学习率按数量级往下扫,看损失是否稳定下降;再做梯度裁剪和标准化。如果损失变成 nan,优先查除零和对数零,其次查学习率。
5.3 现象三:把「模型记住了」当成「模型学会了」
现象:验证指标不错,上线后效果断崖式下跌。
原因:验证集和训练集来自同一批采集条件,模型学到了采集条件带来的伪相关。比如所有正样本都来自同一个时间段,模型实际学的是时间特征而不是业务特征。另外,验证集被反复用来调参,等于被间接训练了,指标虚高。
解决:按时间或实体切分而不是随机切分;留一份完全不参与调参的测试集,只在最后评估一次;对关键特征做消融,看模型到底依赖了什么。
5.4 现象四:只看一个指标就下结论
现象:准确率 0.95 看着很好,实际业务里几乎不可用。
原因:类别不平衡时准确率会严重失真。正例只占 5% 的数据集,全判负例也能拿到 0.95。准确率、精确率、召回率、F1、AUC 各自衡量不同侧面,只看一个必然偏。
解决:先画混淆矩阵,再看和业务代价匹配的指标。漏判和误判代价不对等时,阈值要按代价调,而不是默认 0.5。
5.5 现象五:把过拟合当成 bug 去修
现象:一看到训练验证有差距就开始加正则、减层数,结果验证指标反而变差。
原因:训练验证存在差距是正常的,泛化误差由偏差和方差共同构成。适度差距说明模型有容量去拟合规律;把它压到零,往往是把模型压成了欠拟合。真正常见的错误方向是「修得过头」。
解决:先判断差距的量级和随样本量的变化趋势。样本量增加时差距收窄,说明是方差问题,值得处理;差距固定在某个水平不随样本量变化,多半是偏差问题,该换模型或加特征。
6. 取舍的边界:这门课为什么把数学推导和跑代码放进同一份讲义
一份概述性质的讲义,最容易犯的错是两个极端:只讲概念,学完能背定义但写不出一行能跑的代码;或者只堆代码,跑通了却说不清为什么这么设。这份资料把「为什么学 / 是什么 / 怎么学」三段并置,本质上是在做一次取舍——它不追求把每个算法讲全,而是先把动机、边界、实现路径这三件事挂上钩,让你后面自学任何具体算法时知道该往哪放。
这个取舍直接体现在内容编排上。异或问题放在很靠前的位置,不是为了讲感知机,而是用它把「假设空间」这个概念钉死:模型的表达能力有硬边界,边界之外调参无用。三次浪潮的时间线放在定义之后,也不是为了讲历史,而是用两次失败说明「知识由人编码」这条路的上限。两处安排如果换成常规顺序——先罗列算法、再补历史——读者就只会得到一堆并列的名词。
代价也很明显。概述类材料必然在数学推导上做减法,比如反向传播只给链式法则的直觉而不做完整矩阵推导,正则化只提作用不提解析解。这个减法在读的时候没感觉,等到自己动手调模型时就会暴露:你知道该加正则,但不知道该加在哪一层、系数取多大、和什么冲突。所以我的建议是把这份材料当索引而不是终点,每读到一个机制,就自己补一段可运行的验证代码。
验证的方法我一般这么走。第一步,最小可复现:找一个人为构造的小数据集(异或、线性可分的两簇点、带标签的简单时序),把机制跑出来,确认现象和讲义描述一致。第二步,扰动参数:改学习率、改样本量、改模型容量,观察指标往哪个方向动,验证你对原因的判断是否成立。第三步,构造反例:故意制造数据泄漏、类别不平衡、分布偏移,看自己能不能提前预判指标会怎么变。三轮走下来,一个机制才算真正吃进去,而不只是看懂。
注意:概述材料里的结论多半有前提条件,比如「树模型不需要标准化」只在树只用特征相对顺序时成立,「深层网络表达能力强」只在有足够样本和合适优化时成立。读的时候顺手记下前提,比记住结论有用。
有个教训我印象很深。早年做一个小规模分类任务,训练集和验证集指标都很漂亮,我直接提交了结论,结果换一批数据后全线崩掉。回头查才发现,预处理阶段对全量数据做了标准化,验证集的均值方差信息顺着两个标量漏进了训练过程。这件事之后我每次搭流程,都会强制把「统计量只在训练集拟合」这一条单独拎出来检查一遍,哪怕是三行代码的小脚本也不跳过。机器学习里的坑大多不在算法本身,而在这类看起来无关紧要的边界上。希望帮到你。
本文还有配套的精品资源,点击获取