ChatGPT的横空出世让人工智能成功地吸引了大量的注意力,变成了整个2023年科技圈的最热话题。笔者从事的客户服务管理的工作,日常的工作中也需要处理一些技术相关问题,以此为契机,阅读了一些机器学习和深度学习的文章和书籍,希望可以更好的认识和理解深度学习和人工智能,实践是学习的最好手段,于是尝试学习并自己搭建一个深度学习的神经网络去实现简单的图像分类识别功能。这个过程相当于程序员在学习一门语言时写下的第一行“\underline{Hello World}” ,虽然过程很简单,却是入门的必经之路。
想要训练一个模型,有几个因素是必不可少的:训练数据集(Dataset),算法模型,算力设备。简单说就是在算力设备上用数据集训练一个算法模型,得到训练好模型,然后通过模型去实现人工智能的设定功能(分类,识别,生成等)。在我要实践的这个场景里面,分别是:MNIST训练集,卷积神经网络,算力设备(ThinkPad笔记本),这个模型通过学习训练集里面的数据,对手写数字图片(0-9)进行分类,训练后的模型可以在导入新的图片后识别图片中的数字内容。后面会详细描述实现的过程。
MNIST (Modified National Institute of Standards and Technology database) 数据集是由美国国家标准和技术研究所提供的一套手写数字训练集,被广泛运用于机器学习领域,这个训练集由来自 250 个不同人手写的数字构成, 其中 50% 是高中学生, 50% 来自人口普查局 (the Census Bureau) 的工作人员。包含一个60K的训练集和一个10K的验证集。
需要针对这个训练集做一些进一步的说明,方便理解计算机是如何学习对一些看似只有人类可以处理的文本、图片、声音等数据。众所周知,计算机是一个运算设备,专长是进行数字进行运算,如果让机器学习并处理人类的数据信息,第一步就是要对信息进行编码(特征提取)。比如在Transformer模型里面就是对输入的文本信息进行Embedding。MNIST数据集由是一组28X28像素的手写数字的图片构成的数据集,所以编码的方式是通过向量化转化图片的特征信息。
每一账图片相当于一个28X28数组,其中白色的区域是0,完全黑色的区域是1,灰色的区域,根据灰度的不同在0-1之间进行取值,将数组展平后得到一个784维的向量,这样就实现了图片信息的向量化。并且这个向量对应的分类是0-9之间的数字。
ChatGPT 的模型是一个Decoder-Only的Transformer架构,通过无监督学习的预训练在自然语言处理(NPL)领域取得了巨大的成功 [2] , 但在计算机视觉(CV)领域,尤其是训练数据量较小的情况下,卷积网络仍然具备一定得优势。并且CNNs的可解释性要更强,比较适用于一些特征提取的任务领域。为了可以更迅速和直观的体验训练好的模型效果,选择了更加传统的卷积神经网络来实现深度学习的入门实践。
首先简单介绍一下卷积神经网络(英语:convolutional neural network,缩写:CNN)是一种前馈神经网络,这点和Transformer并没有本质的区别,由一个或多个卷积层和顶端的全连通层(对应经典的神经网络)组成,这一结构使得卷积神经网络能够利用输入数据的二维结构。一个卷积神经网络包括以下几层:
该层要做的处理主要是对原始图像数据进行预处理,如上面对数据集的介绍。
主要的目的是提取输入数据(如图像)的特征。通过一组可学习的过滤器(也称为卷积核或滤波器)扫描输入数据。每个过滤器负责从数据中提取一种特定的特征(如边缘、角点、纹理等)。进而得到一系列特征图(Feature Maps),即过滤器应用于输入数据后的结果,它们包含了输入数据的空间特征信息。
引入非线性,使得网络可以学习更复杂的函数。常用函数:ReLU(Rectified Linear Unit)是最常用的激活函数,它将所有负值置为0,从而增加了网络的非线性。
这一层减少特征图的空间大小,减少参数数量和计算量,同时使特征具有一定的平移不变性。
这层将学习到的局部特征综合起来进行最终的分类或回归。特征图被展平成向量,并通过全连接的神经网络层进行处理。输出层通常会输出每个类别的概率分布(如使用Softmax函数),用于分类任务。
通过过滤器的优化权重,使用如梯度下降等优化算法。计算损失函数的梯度并更新网络权重来最小化训练数据上的损失。
这里简单说一下梯度的概念。训练过程本身是对权重集的搜索,使神经网络对于训练集具有最小的误差即:神经网络预测结果和实际结果保持一致,训练的过程就是运用算力尝试各种可能的权重组合,来确定在训练期间提供最小误差的权重。计算误差函数的梯度可以确定训练算法应增加,还是减小权重。也就是通过梯度来确认权重的调整方式。
整个CNN的数据处理流程可以参考下面的图片:
在了解了模型训练方法、数据集及卷积神经网的一些基础知识之后,我们就可以动手编写代码实现深度学习的神经网络了。这里主要使用的是Python语言和PyTorch框架来进行实践。
Python作为数据分析和人工智能领域中最流行的语言将作为程序编写的语言,Python不光语法简洁灵活也提供大量的数据分析和人工智能的库供编程者使用。深度学习的框架选择使用PyTorch,可以将整个实现过程变得更加简单。
首先导入程序执行需要的依赖包,以及通过创建DataLoader加载MNIST训练数据集
import torch
from torch import nn, save, load
from torch.optim import Adam
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor
from PIL import Image
train = datasets.MNIST(root="data", download=True, train=True, transform=ToTensor())
dataset = DataLoader(train, 32)
接下来是创建一个卷积神经网络的基类,用于实现深度学习。先看一下代码:
class ImageClassifier(nn.Module):
def __init__(self):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(1, 32, (3,3)),
nn.ReLU(),
nn.Conv2d(32, 64, (3,3)),
nn.ReLU(),
nn.Conv2d(64, 64, (3,3)),
nn.ReLU(),
nn.Flatten(),
nn.Linear(64*(28-6)*(28-6), 10)
)
def forward(self, x):
return self.model(x)
self.model = nn.Sequential(...)
这里定义了一个序列化的神经网络(nn.Sequential),该网络按照顺序执行一系列层。以下是网络层的配置:nn.Conv2d(1, 32, (3,3))
第一个卷积层,使用32个3x3的过滤器从1个输入通道(使用的是灰度图像)提取特征。这一层的输出将有32个特征图。nn.Conv2d(32, 64, (3,3))
第二个卷积层,使用64个3x3的过滤器从32个输入通道提取特征。输出为64个特征图。nn.Conv2d(64, 64, (3,3))
第三个卷积层,同样使用64个3x3的过滤器,输入和输出都是64通道。nn.ReLU()
激活函数,用于增加模型的非线性,ReLU函数将所有的负值置为0nn.Flatten()
将多维特征图展平成一维向量,以便可以通过全连接层(线性层)进行处理。nn.Linear(64*(28-6)*(28-6), 10)
全连接层,将展平后的特征向量映射到10个输出节点,对应于10个分类类别(0-9的对应数字)。这里输入图像的大小是28x28像素,通过三次卷积(每次减少2个像素),大小变为(28-6)x(28-6)。def forward(self, x)
这是模型的前向传播函数,定义了输入数据x通过模型的路径。当模型被调用时,forward函数会自动执行。return self.model(x)
在前向传播中,输入x通过上面定义的self.model(即序列化的神经网络模型),并返回模型的输出clf = ImageClassifier().to('cpu')
opt = Adam(clf.parameters(), lr=1e-3)
loss_fn = nn.CrossEntropyLoss()
CNN类创建好之后就需要对类进行实例化并制定配置。这里将模型移到CPU上运行。在深度学习中,模型的运行设备可以是CPU或GPU。使用 .to('cpu')
将模型放在CPU上,如果有GPU可用,也可以使用 .to('cuda')
将模型放在GPU上,由于GPU并行计算的特性,运算速度会快很多,我使用的是一台办公笔记本,所以只能用CPU进行运行,但由于训练量不算大,并且模型本身不复杂,所以跑完10个Epochs的时间大概不到半小时。
还需要创建了一个优化器(opt),用于更新神经网络中的权重(之前提到的通过测算梯度来调整权重)。参数 clf.parameters() 表示优化器将更新clf模型中的所有可训练参数。
机器学习有多个优化器可供选择,包括: SGD,SGDM,Adagrad,RMSProp,Adam,这里用到的是Adam优化算法。Adam是SGDM和RMSProp的结合,是一种常用的梯度下降优化算法,应用比较广泛,主要解决随机小样本、自适应学习率、容易卡在梯度较小点等问题,这里Pytorch已经将优化器实现完成,我们直接调用就好。
lr=1e-3 表示学习率(learning rate),它是优化算法用来控制权重更新步长的超参数。这里设置学习率为1e-3,参数可以根据实际情况进行调整。
然后是创建一个交叉熵损失函数,这里调用的是PyTorch中内置的交叉熵损失函数。在训练过程中,会将模型的输出和真实标签传递给这个损失函数,然后通过反向传播更新模型的权重以最小化损失。
if __name__ == "__main__":
for epoch in range(10):
for batch in dataset:
X,y = batch
X, y = X.to('cpu'), y.to('cpu')
yhat = clf(X)
loss = loss_fn(yhat, y)
opt.zero_grad()
loss.backward()
opt.step()
print(f"Epoch:{epoch} loss is {loss.item()}")
with open('model_state.pt', 'wb') as f:
save(clf.state_dict(), f)
这里定义了10个Epochs,表示整个训练过程将循环进行10次。每个Epoch代表对整个训练数据集的一次完整遍历。epoch 的数量与训练数据量之间存在一定的关系。如果数据集较大或较为复杂,需要更多的epoch来充分遍历数据集,以避免欠拟合,但这个数值也不是越多越好,每次训练都会消耗时间和算力,但模型后续的提升可能比较有限,一般会先训练10-50次看一下模型的整体效果如何。
Epoch由一个或多个Batch组成,在我定义的这个Batch里,首先加载数据集的数据,每个批次包含输入图像数据 X 和相应的标签 y(y是对应的图片的数字标签也就是0-9之间的数字),再将输入数据和标签分别赋给变量 X 和 y。
输入数据和标签都是选择在CPU上进行处理,使用图像分类器模型(clf) 对输入数据 X 进行预测,得到预测结果 yhat。
在使用定义的损失函数loss_fn(yhat, y)
计算模型的预测结果 yhat 与实际标签 y 之间的损失值。
opt.zero_grad()
将优化器的梯度缓冲区清零,以准备计算新一轮的梯度。
loss.backward()
对损失值进行反向传播,计算各个参数对损失的梯度。
opt.step()
根据梯度更新模型的参数,实现优化过程。
这个过程可以简化为:加载数据,导入,预测,清零,反向优化 这样一个过程,循环这个过程,就可以不断的训练和提升模型。
为了了解模型的学习进展需要将每次训练的损失值打印出来,以便看到每次训练的进展以及将模型状态。
最后就是调用PyTorch中的 save 函数将图像分类器模型的状态字典保存到文件中,这个状态字典包含了模型的权重参数。模型文件将以二进制的方式存储为本地文件(model_state.pt)。这样在训练结束后就可以加载这个文件来测试和使用已训练好的模型。到这里模型的训练程序就编写完成了。
接下来就运行程序开始对模型进行训练了。运行程序后主要是下载训练数据集和训练模型计算LOSS两个过程。下面是运行结果的截图:
为了更直观的了解每次训练的LOSS变化,可以在编写一个简单的Python程序用matplotlib将10次训练损失数据导入,看一下损失率的变化。(这个代码就不提供了不是这个文章的重点)观察发现前四次模型的损失下降比较明显,后面几次模型的改进幅度就不是很大,这个与模型本身的复杂度和数据集的大小有关,按这个结果,其实对模型训练5次也是可以的。
每个Epoch的时间大约2-3分钟,我的基本配置是8代i5配上8G内存,整个过程大约在25-30分钟,这个过程不需要任何操作,当Epoch9的LOSS值打印出来就意味着:模型的训练过程结束了,Bravo!!! 😀 成果是:我们得到了训练好的模型文件:
model_state.pt
文件大小是1.39mb,在如今大模型流行的年代,小的微不足道。
之后就可以利用训练模型去验证一下学习的效果和模型本身的分类能力。这里需要需要对模型的训练代码稍加调整。
if __name__ == "__main__":
with open('model_state.pt', 'rb') as f:
clf.load_state_dict(load(f))
img = Image.open('test_img.jpg')
img_tensor = ToTensor()(img).unsqueeze(0).to('cpu')
print(torch.argmax(clf(img_tensor)))
在测试模型之前,需要自己手动创建一个测试数据,用图片处理软件如Photoshop或者画图之类的软件,建立一个28X28像素黑色背景的8bit位图,然后手写一个数字进去保存成test_img.jpg,图片应该和代码程序保持在同一目录下,或者在程序里修改图片的存储路径。
.unsqueeze(0)
用于在第0维上添加一个维度,因为模型一般接受形状为 (batch_size, channels, height, width) 的输入,而单张图像的形状为 (channels, height, width)。.to('cpu')
将张量移动到CPU上进行推理。经过反复几次的试验,发现预测的结果整体可以保持在90%左右的正确率,只要写的不是特别奇怪,基本可以正确预测。考虑到模型本身的大小,这个结果也可以接受。当然如果想进一步提升模型的效果可能需要加大训练集并增加训练的循环次数,这个不在本次的讨论范围之内。
整个CNN的模型训练代码只有50多行,并且可以在绝大多数计算机设备上训练和运行,即使之前没有这方面的知识基础,半天时间也基本可以玩转。通过这个实践,帮助我们了解和感受深度学习的基本原理和实现过程,适合对人工智能、机器学习有兴趣的非专业人士自己动手尝试感受一下。
在这个过程中我们可以看到:深度学习和人工智能本身的训练和算法结构其实并不复杂,但模型最终得表现很大程度也取决于训练的数据集、算力以及对算法参数的调优(Fine Tune),这部分往往需要比较大的投入,所以目前的LLM领域主要是一些巨头公司之间的竞争,作为普通的用户简单了解这背后的原理和实现过程已经足够了,如果对这块有兴趣并希望可以进一步的学习则需要了解更多的数学及计算机专业的背景知识。
[1] MNIST Dataset | Papers With Code:
https://paperswithcode.com/dataset/mnist
[2] Improving language understanding with unsupervised learning:
https://openai.com/research/language-unsupervised
[3] An Overview of Convolutional Neural Networks | Papers With Code : https://paperswithcode.com/methods/category/convolutional-neural-networks
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。