机器学习的核心:如何从数据中找出函数?
今天这堂课,我们将用一堂课的时间搞懂机器学习和深度学习的基本概念。到目前为止,我们已经探讨了生成式人工智能的基本原理:我们有一个函数 F,它接收一个未完成的句子 X 作为输入,然后输出下一个 Token,即 F(X)。在过去的课程中,我们详细剖析了 F 的内部结构及其运作方式,但始终没有讲解 F 是如何被找出来的。
今天,我们就要揭示这个核心问题:这个能够预测下一个 Token 的函数 F,是如何通过数据被寻找出来的。通过数据找出一个函数的这项技术,就统称为机器学习(Machine Learning)。
今天的课程将分为两部分:首先讲解机器学习的原理,然后通过一些实作案例,帮助大家更深入地理解所学内容。
为了让大家明白机器学习这项技术的普适性,我们今天不以语言模型为例,而是选择一个完全不相关的例子。我们要寻找一个函数,它的输入 X 是一份李宏毅老师的课程投影片,输出 Y 则是老师讲完这份投影片所需的时间(一个数字)。在机器学习中,当输出是一个数值时,这类任务被称为回归(Regression)。
这个函数有什么用呢?它可以回答一个大家上课时最关心的问题:“老师什么时候要下课?”有了这个函数,你只需将当前的投影片输入,它就能告诉你这堂课大概会持续多久。
机器学习三步骤:定义目标、划定范围与寻找最佳解
机器学习寻找函数的过程可以概括为“3+1”个步骤。我们先讲前三个核心步骤,这三步合起来就是学习(Learning)或训练(Training)的过程。
- 我要找什么样的函数?(定义目标)
- 我有哪些函数可以选择?(划定范围)
- 如何找出最好的函数?(执行优化)
这三个步骤中,第一步和第二步没有固定的先后顺序,但理解了这两步之后,才能进入第三步。
步骤一:定义目标 (Defining the Goal)
第一步是确定评估一个函数好坏的标准。假设有人给了我们一个函数 F,我们需要有能力判断它是否符合我们的要求。这与我们上周讲的“生成式人工智能能力检定”非常相似。评估一个模型通常需要准备一些输入,让模型产生输出,然后将输出与标准答案进行比较,计算它们之间的某种距离或相似度。所有这些度量的平均值,我们称之为评估指标(Evaluation Metric)。
在机器学习的第一个步骤中,我们做的事情和评估几乎一样。这也是为什么我们先讲评估,再讲机器学习原理。
具体到我们的例子,我们有一些老师过去上课的投影片(例如“机器学习2021”课程)以及对应的实际讲课时长。我们将这些投影片输入到某个给定的函数 F,它会预测一个时长。然后,我们计算预测时长与真实时长的差距。例如,某节课实际讲了100分钟,F 预测了99分钟,差距的平方就是1。我们将所有课程的差距平方取平均值,这个数值就代表了函数 F 的好坏。
因为我们计算的是差距,所以这个数值越小,代表函数预测得越精准。当评价指标是越小越好时,我们称之为损失(Loss)或成本(Cost);如果是越大越好,则称为目标(Objective)。在本课程中,我们主要使用 Loss 的概念。
我们这里计算预测时长与真实时长差距的平方的平均值,这个方法叫做均方误差(Mean Squared Error,简称 MSE)。这个 Loss 定义了一个函数的好坏。用于定义 Loss 的这些数据,包括输入的投影片和对应的真实上课时长,合起来就叫做训练数据(Training Data)。
步骤二:划定选择范围 (Defining the Function Set)
第二步是确定我们有哪些候选函数可供选择。由于函数只能处理数字输入,我们不能直接把投影片丢进去。因此,需要先将投影片转换成一系列数字,这些数字被称为特征(Feature)。例如:
- x1: 投影片的页数
- x2: 投影片的总字数
- x3: 投影片标题的长度
- x4: 投影片中是否提到“learning”这个词(0或1)
接下来,我们要假设函数 F 的形式。一个直观的假设是,课程时长 y 与投影片页数 x1 存在线性关系:y = w1 * x1 + b。
w1代表每页投影片平均讲解时间的比例。b可能代表每次上课固定的开场白时间。
w1 和 b 是我们不知道的具体数值,在函数中,这类未知数值被称为参数(parameter)。当我们写下 y = w1 * x1 + b 这个式子时,由于 w1 和 b 未知,它实际上代表了一个无限大的函数集合。这个集合中的每一个点(每一组特定的 w1 和 b)都对应一个具体的函数。
这个函数的范围是由人类根据对问题的理解,即领域知识(domain knowledge)来划定的。我们相信页数和课程时长成正相关,所以选择了这个线性模型。这个由人类定下的函数范围,也就是所谓的模型(model)。模型是对复杂现实世界的一种简化理解。
步骤三:寻找最佳函数 (Finding the Best Function)
有了计算 Loss 的方法和候选函数的范围后,第三步就是在我们划定的范围内,找出一个能让 Loss 最低的函数。
我们的 Loss 函数可以写成:L(w1, b) = (1/n) * Σ( (w1*xi + b) - yi_hat )^2。其中,n 是课程总数,xi 是第 i 堂课的投影片页数,yi_hat 是第 i 堂课的真实时长。
这个 Loss 本身也是一个以 w1 和 b 为参数的函数。我们的目标是找到一组 w1* 和 b*,使得 L(w1*, b*) 的值最小。这个问题就转化成了一个优化(optimization)问题。
最直接但最笨的方法是暴力穷举。我们可以设定 w1 和 b 的一个可能范围,然后遍历所有组合,计算每个组合对应的 Loss,最后选出 Loss 最小的那一个。对于只有两个参数的简单问题,这是可行的。通过这种方法,我们可以绘制出损失表面(Loss Surface),这是一个三维图形,展示了不同 w1 和 b 组合对应的 Loss 值。
然而,在真实场景中,模型可能非常复杂,参数成千上万,暴力搜索不可行。因此,我们需要一个更通用的优化方法。
核心算法:梯度下降法 (Gradient Descent)
一个几乎适用于所有模型和 Loss 函数的通用优化方法叫做梯度下降法(Gradient Descent)。
为了简化说明,我们先假设只有一个参数 w1。梯度下降法的思想很简单,就像“饿狼下坡”:
- 随机选择一个起始点
w0。 - 在这个点,判断往左走还是往右走能让 Loss 下降。
- 朝着 Loss 下降的方向迈出一步,到达新位置
w1。 - 重复这个过程,直到走到一个“山谷”的凹点,无论往左还是往右,Loss 都不会再减小,此时就停下来。
这种方法的一个明显缺点是,它可能会停在一个局部最小值(local minimum),而不是我们真正想要的全局最小值(global minimum)。
在实际操作中,我们不是真的去试探左右两边,而是计算当前位置的切线斜率。
- 如果斜率是负的(左高右低),就向右走。
- 如果斜率是正的(左低右高),就向左走。
- 如果斜率是零,就停下来。
这个切线斜率,就是 Loss L 对参数 w1 的偏微分。幸运的是,现代的深度学习框架(如 PyTorch、TensorFlow)可以自动计算这些偏微分,我们无需手动推导。
每一步要迈多大,取决于两个因素:
- 学习率(learning rate):一个人为设定的超参数,代表学习的速度。学习率太大可能导致“跑飞”,永远无法收敛;太小则学习过程会非常缓慢。
- 切线斜率的大小:山坡越陡,步伐就越大。
当有多个参数(如 w1 和 b)时,我们会计算 Loss 对每个参数的偏微分,这些偏微分集合起来形成一个向量,称为梯度(gradient)。然后,我们沿着梯度的反方向更新所有参数。这个“计算梯度 -> 更新参数”的过程会反复进行,每一次更新称为一次迭代(iteration)或更新(update)。
训练中的挑战与技巧
在实际训练中,我们会遇到一些挑战,并需要运用一些技巧来应对。
批次 (Batch) 与 周期 (Epoch)
计算整个训练数据集的 Loss 和梯度可能非常耗时,特别是当数据量巨大时。为了解决这个问题,我们通常会将数据分成一个个的小份,称为批次(batch)。模型每处理完一个 batch 的数据,就计算一次梯度并更新一次参数,而不是等所有数据都处理完。
- 周期 (Epoch):当模型看完了训练数据中的所有 batch 一次,就称为完成了一个 Epoch。
- 随机梯度下降 (Stochastic Gradient Descent, SGD):当 batch size 设为1时,就是SGD。
使用 batch 的好处是更新速度快,但缺点是每次更新的方向可能不太稳定,因为只基于一小部分数据。因此,batch size 的选择是一个需要权衡的超参数(hyperparameter),即需要人为设定的参数。此外,在每个 Epoch 开始前,通常会对数据进行洗牌(shuffle),以增加随机性,避免模型反复看到同样组合的 batch。
关键的“+1”步骤:验证 (Validation)
当我们通过训练找到了一个看似不错的函数后,还不能直接拿去用。我们需要进行一个额外的步骤——验证(validation)。
验证好比是模拟考,而最终的应用或比赛则是真正的大考。模拟考可以多次进行,考得不好可以回去复习调整;大考则通常只有一次机会。
我们需要准备一份独立的验证数据集(validation set),这份数据没有参与训练过程。当我们用训练好的模型在验证集上测试时,如果结果很差,就需要回头检查前面的三个步骤哪里出了问题。
例如,我们用“机器学习2021”的数据训练出的模型,在预测“生成式AI导论2025”这门课的时长时表现很差。通过分析发现,两门课的性质不同(一门是专业课,一门是导论课),导致同样页数的投影片对应的讲课时长有系统性差异。这说明我们第一步就错了——训练数据与实际应用场景不匹配。解决方法是更换训练数据,使用性质更接近的“生成式AI导论2024”的数据重新训练。
从线性模型到深度学习:构建更强大的函数集
如果更换数据后,验证结果依然不理想,可能是第二步——我们划定的函数范围太小了。线性模型 y = w1*x1 + b 只能表示一条直线,但真实世界中变量间的关系可能要复杂得多。
为了构建一个能逼近任意复杂函数的范围,我们可以引入深度学习(Deep Learning)的思想。
- 任何复杂的曲线都可以用一系列短的直线段(Piecewise Linear Curve)来逼近。
- 任何由直线段组成的曲线,又可以由一个常数项和多个“山坡形状”的函数叠加而成。
- 这种“山坡形状”的函数,又可以由两个更基础的、只有一个转折点的函数组合而成。
- 这个基础的函数,其数学形式为
max(0, wx+b),它有一个专门的名字,叫做修正线性单元(Rectified Linear Unit,简称 ReLU)。
一个接收输入、乘以权重、加上偏置、再通过 ReLU 函数的计算单元,被称为一个神经元(Neuron)。多个神经元并排组成一个层(Layer)。将多个层堆叠起来,就构成了神经网络(Neural Network)。当网络中包含很多隐藏层(Hidden Layer)时,就是所谓的深度学习。
理论上,只要有足够多的神经元,神经网络可以模拟任何函数。在神经网络中计算梯度的有效算法被称为反向传播(Backpropagation)。
深入优化与过拟合的挑战
当我们使用更强大的神经网络模型时,又会遇到新的问题。
首先是优化困难。神经网络的损失表面非常复杂,除了局部最小值,还可能卡在鞍点(Saddle Point,即梯度为零但并非最小值的点)或者梯度极其平缓的区域,导致训练停滞。这需要我们通过调整超参数(如学习率、batch size)或使用更高级的优化器(如 Adam)来解决。
其次,一个更严重的问题是过拟合(Overfitting)。当我们用一个非常强大的模型(函数范围非常大)去拟合训练数据时,模型可能会“死记硬背”训练数据中的每一个细节,而不是学习到底层的规律。这会导致模型在训练数据上表现极好(Loss很低),但在未见过的验证数据上表现极差(Loss很高)。
这就像在驾训班学车,你不是学习看路况开车,而是记住了“看到后照镜里某个贴纸时方向盘左打四分之一”这样的口诀。在同一个场地(训练集)考试你次次满分,但换一个场地(验证集)就完全不会开了。
解决过拟合的一个常用方法是早停(Early Stopping)。在训练过程中,我们不仅监控训练集上的 Loss,也同时监控验证集上的 Loss。当训练 Loss 持续下降,但验证 Loss 开始上升时,我们就及时停止训练,并选用验证 Loss 最低时的那个模型。
验证与测试的陷阱:避免对测试集过拟合
在模型开发过程中,我们会反复使用验证集来调整模型和超参数。但这样做也存在风险:我们可能会在不经意间对验证集本身产生过拟合。
最终,我们需要一个从未在开发过程中使用过的测试集(Test Set)来对模型进行最终的、一次性的评估。然而,在学术界和工业界的许多基准测试(Benchmark)中,由于测试集可以被反复提交测试,长期下来,研究社区整体也可能对测试集产生过拟合。这就是为什么有时AI在某些榜单上的分数超过人类,但在实际应用中却表现平平。
为了缓解这个问题,许多机器学习竞赛会将测试集分为公开集(Public Set)和私有集(Private Set)。参赛者可以看到在公开集上的排名,但最终成绩由在私有集上仅有一次的测试结果决定。
实作环节:在代码中重现学习过程
在课程的后半部分,我们通过一个代码实例,完整地重现了上述学习过程:
- 数据准备:收集了不同课程的投影片页数、字数和实际讲课时长。
- 线性模型训练:实现了暴力穷举和梯度下降法来寻找最佳的
w和b。 - 验证与迭代:展示了更换训练数据对验证结果的改善。
- 特征工程:通过添加“平均每页字数”这一新特征,提升了模型性能。
- 神经网络训练:构建并训练了一个简单的神经网络,并展示了优化过程的困难以及过拟合现象。
- 最终测试:运用早停策略,选出了在验证集上表现最好的模型,并用它来预测当天课程的真实时长。
最终,模型预测的时长与实际时长惊人地一致,为这堂关于预测的课程画上了一个圆满的句号。