大家好,欢迎回来。今天,我们的一些助教会进行演示。还不认识的同学注意了,这位是David,我课题组的学生,也是这门课的助教。另一位助教Chanaka稍后会出场。请大家记住他们的样子,因为他们会负责答疑时间,并帮助大家完成项目,也会主持阅读和讨论环节。
在他们开始前,先说几件琐事。首先,请确保你已经提交了项目提案。有一个简短的表格,需要你填写队友信息和项目偏好。我已经在Piazza上分享了大家的回复。如果你还没找到队友,想组建团队,可以去看看那个表格,找找兴趣相近的同学。
另外,第一个任务是在下周四进行项目提案的演示。到时候,每个小组需要确定一个想法,并用5分钟时间快速介绍你们打算做什么、有什么想法、会用哪些数据集。课程会给出反馈,相关说明和评分细则都在Piazza上。之后一周,你们需要提交一份关于该提案的简短报告,也就是文字版,方便我们阅读和提供反馈。所以,记得填表、找队友,并开始着手准备项目提案的演示和报告。
今天,我们会为大家提供一些实用技巧的教程,涵盖如何使用PyTorch、构建模型、迭代以及调试机器学习模型。
那开始吧?David,交给你了。
谢谢介绍。我是David,这门课的助教。首先我会讲如何使用PyTorch,然后基于一个非常简单的项目,讲讲如何一步步进行。开始之前,请先打开这两个链接,你会看到两个代码页面,可以跟着我一起操作。今天的教程主要针对PyTorch新手。如果你已经熟悉PyTorch,这部分可能帮助不大,但我会尽量安排好结构,希望每个人都能有所收获。我们开始讲PyTorch。大家都能打开这个Colab笔记本吗?
首先,非常简要地介绍一下PyTorch。它是一个加速机器学习流程的库。PyTorch的核心是张量(Tensor),可以理解为线性代数中多个矩阵的集合。
我们需要先导入PyTorch。然后看一些基本操作。初始化张量有几种方式:如果你有一个二维数组,比如[1,2,3,4],可以直接调用`torch.tensor`来初始化。如果你用过NumPy,也可以把NumPy数组转换成Torch张量。
我们先快速过一遍基础内容。我们可以初始化特定形状的张量,比如要一个2x3的矩阵,可以用`torch.rand`(随机张量)、`torch.ones`或`torch.zeros`来初始化全1或全0的张量。
张量有不同类型,可以用`tensor.type()`查看。一个常见的错误来源就是张量类型不是你预期的。如果你初始化了一个整数类型的张量,就不能把它和浮点类型相乘后再存回整数类型,因为不兼容。所以加载数据时,记得用`tensor.type()`检查类型,可以用print,或者用调试器。
另外,张量可以运行在不同设备上,比如CPU或GPU。用`tensor.device`可以查看张量所在的设备,这也是常见的错误点。比如两个张量一个在CPU,一个在GPU,就不能直接相乘。需要用`tensor.to(device)`把张量移动到你想计算的设备上。
还有一些简单的张量运算。比如两个张量x和y,可以用`x+y`相加。注意,`x*y`是逐元素相乘,不是矩阵乘法。矩阵乘法要用`torch.mm`。这些就是PyTorch张量运算的简要概览。
接下来,我会用我们实验室收集的一个小型数据集,带大家完成一个小项目。这个数据集来自一些气体传感器,采集了特定气味的数据。这个简单教程很适合用来完整走一遍训练、数据处理、评估等整个流程。
在深入模型之前,我们首先得看看数据。这一点我强烈建议:数据格式可能不是你期望的样子。无论是自己收集还是从网上下载的数据,都很常见。文档上说是一种格式,下载下来却是另一种。所以在建模或训练之前,实际查看数据非常重要。
我们运行代码,下载实验室采集的气味数据。这个项目是一个三类分类任务:识别环境空气、酒精和咖啡豆。加载数据后,打开数据文件夹,你会看到一堆CSV文件,对应各个类别。为了简化,我们只用环境、酒精和咖啡豆这三类。然后用`os.listdir`列出所有文件,再打开一个文件看看内容。这一步非常重要。从数据中你会看到有很多列,对应不同的气体传感器。
可视化数据也很重要。处理时间序列数据时,正则化很关键,因为有些数据与分类相关,有些则无关。比如酒精检测的图中,红色区域是传感器的有效响应区间。我们先采集一些环境读数,然后把待测物质靠近气体传感器,采集实际读数。这样能消除环境因素的影响,在这个项目中非常有用。
可以看到,有些气体传感器对物质非常敏感(比如NO2 BLC这类),有些则不相关(比如酒精传感器可能不工作)。还有一些温度、气压读数,对分类任务帮助不大。
提问:这些数据是手动收集的吗?具体怎么采集的?
答:我们有一个开关,可以自动在状态0和1之间切换。状态1代表有物体靠近,状态0代表远离。你可以看到,有些通道与目标物质相关,有些则无关。那应该删掉这些列,还是保留它们参与训练?我的建议是:大多数情况下,不需要删除任何通道。因为如果某个通道对所有类别都不相关,神经网络会自动学会忽略它。但如果模型完全无效,你可以尝试去掉一些通道。不过大多数时候没必要。
提问:如果你知道某个传感器不工作,为什么还要保留它?
答:这个传感器在这个样本里不工作,但在其他样本里是工作的。所以如果要删除通道,通常应该查看所有数据,而不是只看一个文件。
我觉得神经网络对单个通道的噪声通常很鲁棒。如果噪声分散在很多通道,可能问题更大,那时可能需要用PCA等方法来降噪。但我们的数据相对干净,不需要太多预处理。
不过我们确实对每个时间段取了平均值,并减去了环境读数,因为我们发现每个传感器的初始读数在不同实验之间波动很大。所以通过减去环境读数得到相对变化,让数据更稳定。这是预处理的一部分,不是数据采集过程。
另一个发现是,不同读数的量级差异很大。比如有的传感器范围是0-2000,有的是0-5100。这说明我们需要做归一化。这是最重要的预处理步骤之一,因为神经网络对量级变化并不鲁棒。如果一个通道的值高达2000,它很可能会主导整个训练过程。
所以加载、合并数据,分离特征和标签之后,需要进行归一化。我们用scikit-learn的StandardScaler,把输入x变换到(比如0-1)范围,使其更鲁棒。然后按8:2的比例划分训练集和测试集。
接着初始化数据集(Dataset)和数据加载器(DataLoader)。Dataset负责提供数据,DataLoader负责启动多进程工作,通过并行处理加速数据加载。每次请求数据时,它会返回一个批次(batch)的样本,而不是单个样本。这样做的优点是,模型计算出的梯度(即如何更新模型使其更好)会基于多个样本的平均值,更加鲁棒。另一个实用技巧是:在GPU内存允许的情况下,尽量使用最大的批次大小,这样训练过程最稳定。
最后设计模型。这是一个非常简单的MLP(多层感知机)模型,堆叠了几个线性层和随机失活(Dropout)层。我们可以在这里调整层的大小。输入是13维,输出是3维(三类分类)。这两个数字固定,但中间的隐藏层大小可以改。对于这种小型样本数据,两到三层通常就足够了。
提问:如何决定层数和每层大小?
答:很好的问题。一个好的起点是“简单开始”,先用两三层看看效果。之后会有关于调试模型的详细教程,可以再深入讨论。
接着是训练循环。大致流程是:从DataLoader获取数据和标签,把数据输入模型,将模型输出与理想输出比较,计算损失(Loss)。得到损失后,计算梯度,它告诉我们应该如何更新参数来改进模型。最后调用优化器的`step`方法,真正更新模型。这就是完整的训练生命周期。
训练完成后,进行评估。过程类似,只是不再更新模型。我们得到了大约75%的准确率,还算可以。更详细地看每个类别的精确率、召回率和F1分数,模型在环境和酒精上表现不错,但对咖啡豆的识别效果较差。
之后你可以迭代改进模型。先找出哪些类别表现最好、哪些最差。针对最差的类别,分析是不是数据不足,或者其他原因。如果是数据问题,就去收集更多咖啡豆的样本。这就是迭代改进模型的方法。
最后,别忘了保存模型,以便后续分析使用。以上就是关于如何用PyTorch训练一个非常简单模型的教程。
我知道跳过了很多细节,但如果有问题,现在可以问。
提问:窗口和样本?
答:我们采样了多个时间段,每个时间段大约10行,然后把整个读数分割成不同的时间段。分类是在这些10行的片段上做的。这些设计决策在你的应用中可能非常重要,比如窗口选大选小、每个窗口对应的标签是什么、如何划分训练/验证/测试集。例如,如果训练和测试数据来自同一天或同一个样本,模型可能只是记住了样本,而无法泛化到你真正应用传感器的场景。
很多超参数需要你仔细设计和决定。这确实是个好问题。做训练测试划分时,要尽可能贴近真实场景。如果你打算在某一天训练,另一天使用,那么划分就应该符合这个设定。不应该用同一个实验的前半段训练、后半段测试,那样模型表现可能很好,但无法反映真实性能。
另一个例子:我们在实验室某个房间收集数据并训练模型,但换到另一个房间或室外,模型就失效了,因为它没学过不同形状的房间以及温度、湿度等环境因素。这意味着,如果你在一个场景下训练,却要在另一个场景下使用,就必须收集多样化的数据,并相应地划分。
接下来,我快速讲一下用随机森林(Random Forest)进行分类。随机森林不是深度学习模型,它由许多决策树组成,通过多数投票来综合输出。训练非常快。我们划分训练测试集,初始化随机森林分类器(来自scikit-learn),然后评估,得到了95.8%的准确率,远高于之前的深度学习模型。这告诉我们:对于非常简单且噪声不大的数据,简单的模型(如随机森林)有时比花哨的深度学习模型表现好得多。
另一个实用建议:处理低维数据时(每个数据点只有几百个数),表现最好的很可能是浅层模型,比如随机森林或SVM。处理这类数据时,建议先从简单的经典模型开始,再考虑深度学习模型。
提问:既然不同房间测试结果不同,那随机森林对这种变化鲁棒吗?
答:我们还没有另一个房间的数据。但一般来说,简单模型对噪声更鲁棒,因为复杂模型的决策边界非常紧凑,更容易过拟合训练数据中的噪声,从而对环境的改变不太适应。更直接地回答:如果模型在不同地点效果都不好,那可能需要新的数据格式。对于视觉任务,我们有强大的预训练编码器,可能在任何数据上都有效。但对于时间序列数据,跨领域迁移不容易。可能一个简单模型在一个环境下工作良好,但在另一个环境下就不行。PyTorch教程就到这里。
在讲如何调试模型之前,我们快速过一下Hugging Face的教程。Hugging Face不是单一的包,而是一套工具包,让你用更少的代码训练更大的模型。它主要有两个包:Transformers和Datasets。Transformers提供API来初始化带预训练权重的大模型,比如大语言模型。Datasets方便你从网上下载公开数据集。
怎么选模型或数据集?你可以在Hugging Face网站上搜索,看哪些模型在你的任务上受欢迎,选一个,把名字写进代码里就行。还有一些非官方的包也很流行,比如bitsandbytes,它提供量化功能,把大模型变小,更好适配GPU内存。另外Flash Attention包能让训练更快、更省显存。
本教程我们将用LoRA(低秩适配)来微调一个大语言模型。LoRA本质上是一个适配器,通过减少可训练参数来降低训练所需内存。
现在打开Hugging Face教程。在使用模型前,需要登录Hugging Face。运行登录单元格,会弹出网页要求输入token。你可以在Hugging Face网站上创建新token,取个名字,然后复制粘贴过来登录。登录的原因是我们要用的模型需要授权访问。如果你还没获得权限,需要同意协议并申请,通常会自动批准。
然后安装几个包,主要是Hugging Face相关的。在代码中你会看到模型名、数据集名和列名。我们用的模型是StarCoderBase-1B(10亿参数),量化方式是4-bit(来自Anvil的KIV4)。判断一个模型能否装进你的GPU,可以看这个参数量。经验法则是:把参数量的“B”前面的数字乘以2,得到近似的最小推理所需显存(GB)。对于训练,需要的会更多一些,取决于具体方式。
LoRA有一批参数,决定了你对模型改动的大小。R和alpha越大,所需显存越多,改动也越大。
然后一行代码加载数据集,因为时间有限,我们只取4000个样本。接着初始化分词器(Tokenizer),它负责把英文单词转成模型能理解的token。处理这类大语言模型时,你通常不需要自己处理分词器,很简单,调用即可。
数据集部分定义了训练过程如何从原始数据集中获取数据。需要调用分词器把文字转换成token,这是传统模型不需要、但大模型微调必须做的步骤。
然后加载模型。由于内存限制,我们加载4-bit模型(理想情况下16-bit训练更稳定,但Colab上只能4-bit)。接着设置LoRA并开始训练。Hugging Face有个很好的功能,与Weights & Biases(wandb,训练监控工具)深度集成。运行训练后,你会看到wandb的链接。打开wandb页面,登录并粘贴API key,就可以实时看到训练过程。
在监控面板上,我通常关注两个指标:GPU内存分配和GPU功耗。如果GPU内存分配过高(如95%),训练后期很可能会内存溢出。如果过低,说明批次大小太小,训练不理想。建议让GPU内存分配保持在80%-90%之间。GPU功耗则反映GPU的忙碌程度。如果功耗只有10%,说明训练或数据处理管线效率低下,GPU大部分时间空闲。理想情况下,功耗大于60%比较好。如果低于60%,需要检查数据集实现、处理脚本和训练管道,找出慢的原因。
除了GPU使用率,损失值也很重要,我们希望它下降。如果损失不降,说明有问题。不过头几步损失不降很正常,可能因为学习率问题或模型初期不理想。可能几百步后还不降,那就不正常了,训练不会收敛。
另外可以关注梯度范数(Grad Norm),即每一步梯度的平均大小。如果梯度范数很大,说明每次更新步长非常大,有时可能是有意为之,但随着模型收敛到最优点,它应该逐渐减小。如果梯度范数在增加,说明训练过程有问题。
评估部分就不做了,需要再花一小时。我们现在可以讲调试模型的部分了。
调试模型更像一门艺术,而非纯粹的科学。通常写程序时,你能得到明确信号判断程序是否正确。但机器学习模型很难判断什么时候工作、什么时候不工作。这个调试清单就是为了让你有一系列可以逐一检查的项目,理解如何改进模型。如果你从未训练过模型,这听起来可能有点学究,但长远来看很有用。
训练神经网络是门艺术:有些模型能训练,有些不能,没有绝对。我们想揭开这个过程的神秘面纱。你可能会遇到各种奇怪的bug:梯度奇高、梯度奇低、损失无穷大,怎么办?这张幻灯片涵盖了训练模型前建议你先过一遍的要点。
首先是“成为数据的一部分”。作为机器学习从业者,我们总爱用最花哨的模型,但这往往适得其反。最重要的是数据。你应该花大量时间观察数据,理解数据分布,看看自己能否找出模式。大多数情况下,尤其当你没有大语言模型那样的海量数据时,你需要设计能够识别数据中特定模式的模型。所以,观察数据,花大量时间理解并找出可建模的直观模式。
有了直观理解后,尝试建立一个非常简单的模型,得到一些初步信号。比如线性回归或一个简单的分类器。有时你用的数据集在GitHub上可能已经有人尝试解决过,直接拿他们的模型试试看。在PyTorch中,初始权重是随机矩阵。关键是要先固定随机种子(fix the seed),让过程确定化,避免额外的变异性。
尽量简化你的模型,并且时刻关注损失函数的初始化。很多时候初始化损失是无穷大,这有很多原因,检查这个能帮你判断模型是否正常工作。
最后,如果你的模型完全不起作用,尝试让它过拟合(overfit)单个数据样本。也就是让模型记住那一个样本,来判断模型有没有学习能力。如果连这个都做不到,你就需要回到设计更好的模型上。
所以,先做简单模型,在小数据集上过拟合。如果达不到低错误率,就回到观察数据的步骤,或者改进建模策略。过程中要可视化损失的变化,观察损失如何下降、上升或出现异常。同时关注梯度,有时梯度会变成无穷大,可能是因为你量化得不够好,或初始化不正确。这能让你明白应该修复什么来让模型工作。
最重要的建议是:一开始不要太担心模型架构。很多时候我们总想搞个很酷的架构,以为需要研究论文里那些东西。别这样做,这只会适得其反。
做完上述步骤后,要注意:机器学习关心的不是记忆(memorization),而是泛化(generalization)。为了泛化,需要正则化技术,比如Dropout、权重衰减等。只有当你确认模型能在训练集上过拟合之后,再开始做这些。在那之前,把这些正则化项关掉。
最后我再强调一下:大多数情况下,你的模型可能完全正常,只是还没找到正确的超参数。所以尽力去搜索多种超参数,尤其是小模型,找到合适的性能。我见过很多数据科学家直接跑默认参数,得到很差的准确率。但如果仔细调参,往往能得到很好的结果。尝试这样做。如果这些都做了,可以用不同的集成模型,或者让模型训练到收敛之后继续训练(过去收敛点),有时会有帮助。做完所有这些,再开始考虑更复杂的架构。
基本流程就是:先关注数据,建立简单模型,在这些简单模型上尝试过拟合数据。一旦能过拟合,再进行正则化,然后针对你最终的下游任务进行微调。
这就是调试的基本思路和技巧。如果有问题,我很乐意回答。之后我们会分享两篇博客,建议大家都看看,深入理解这个话题。
大家还有问题吗?