前言:
因为还在上学,时间不太够用,很多内容写到后面心有余力不足,未来有时间我会慢慢补充。人工智能的知识涉猎范围广又杂乱无章,啃书或上课学到的知识往往很早就过时了或者离实际的项目无关。所以,我很希望通过这种一个一个“小实验”的方式让大家拎出一个属于自己的“主干知识”,尝到训练模型的“甜头”。
一、Python基础知识:
1 类与对象的关系:
比如定义一个“鸟类”。鹦鹉就是“鸟类”的一个对象,海鸥也是“鸟类”的一个对象。
“类”是静态的,是在程序执行前就已经被定义好的;
“对象”是动态的,它们在程序执行时可以被创建和删除。
2 类的属性:
class Bird:Flight_mode = '滑翔'Reproduction_mode = '蛋生' #类的属性def introduce(self): #类的方法print('爷是只高贵的鸟')
类的属性是全类共有的,比如上例中定义的“鸟类“里的”飞行方式“和”繁衍方式“;
类的方法可以简单理解成在类中写了一个函数,不过要注意的是:在Python中,self是一个表示“对象“自身的参数,通常作为方法的第一个参数。当我们创建一个对象时,Python会自动将该对象作为self参数传递给类的方法。
接下来,请读者思考一个问题,假如我有一只小鸟名叫”Lucky“,基于上面的例子,如果我想打印:
(1)“Lucky的飞行方式是滑翔,繁衍方式是蛋生”应该怎么做?
(2)“Lucky的年龄是一岁大,颜色是黄色的”上例中类的方法应该怎么修改?
解答:
(1)的问题比较简单,因为飞行方式和繁衍方式都是整个“鸟类”共同的属性,直接使用类名调用即可。
print("Lucky的飞行方式是"+Bird.Flight_mode+",繁衍方式是"+Bird.Reproduction_mode)
(2)最大的问题在于,年龄和颜色都是Lucky这个“对象”的属性,这时我们就需要使用类的方法来定义对象的属性。说白了就是在类中写一个函数,让函数中的属性只属于定义的对象。
class Bird:Flight_mode = '滑翔'Reproduction_mode = '蛋生' # 类的属性def __init__(self, name, age, colour):self.name = nameself.age = ageself.colour = colourdef introduce(self):print(self.name,"的年龄是", self.age, ",颜色是", self.colour)p = Bird("Lucky", "一岁大", "黄色的")
p.introduce()
其中,__init__是一个特殊的方法,用于在创建对象时进行初始化。它是Python中的构造函数,用于将属性值赋给对象。
类的基础介绍言尽于此,不多赘述。
参考文章:http://t.csdnimg.cn/Eg9kr
3 类的继承:
继承的主要作用是实现代码的重用。继承使得子类拥有父类的方法和属性。在使用深度学习框架,比如Pytorch时,只有将我们的类继承官方的类,我们才能在官方的类实现的基础上继续做下去。
class animal:def eat(self):print("吃")def drink(self):print("喝")class dog(animal):def dark(self):print("汪汪叫")goudan = dog()
goudan.eat()
goudan.drink()
从上面的代码可以看出,在编写dog类的时候,我们并没有重写eat和drink两个方法。我们只需要在dog后面的括号中加上父类的名字即可。当子类继承了父类,子类就可以直接使用父类中的方法了。在本例中,goudan可以直接使用animal类中的eat和drink两个方法。
参考文章:http://t.csdnimg.cn/sw0VK
二、简单科普:
1 深度学习框架的概念:
如何让计算机识别手写数字0~9?如今的我们只需要一个简单的卷积神经网络(CNN)就可以搞定。
本次我们使用pytorch深度学习框架,使用pytorch中的nn函数来调用深度学习中的基本单元,用pytorch中的nn.squential模块来构建神经网络模型。
深度学习框架的基本属性:
1.将繁琐的计算、操作封装成黑箱,让我们只需要关注输入与输出;
2.把解决问题的流程模块化、模板化;
3.常用的工具封装成库,方便使用。
所以,框架的概念就是模板的概念,就像英语作文模板往往会给你提供好开头、结尾等,让我们只需要根据不同的作文主题更换不同的英语单词一样。
2 张量的概念:
张量,英文为“tensor”,是神经网络的主要数据容器,它包含的数据几乎都是数值。但显然,我们传入神经网络的数据并不只有数字,还有图片、声音、文本等等。如何建立起他们之间的联系?这个我们稍后解释,现在我们先来看看什么是张量。
2.1 什么是张量:
学习过线性代数的话,你可能对矩阵比较熟悉,张量其实就是矩阵向任意维度的推广。
(1)如果一个矩阵只有一个元素,那么它被称为0阶张量;
(2)如果一个矩阵只有一行或者一列(我们称其为只有一个“轴”),则被称为一阶张量;
(3)显然,大部分的矩阵都有m行n列(有两个轴),则就被称为二阶张量;
(4)如果将一个二阶张量看作一个整体,由多个二阶张量组成一个新的数组,则二阶张量就被推广为三阶张量;
当然,我本人有个小技巧帮助你推导三阶之后的张量。来,跟我念:一个张量、往下拉、往右拉、往后拉;看成一个整体、往下拉(四阶)、往右拉(五阶)、往后拉(六阶);看作一个整体.......以此类推。
当然,在实践中,我们很少遇到三阶之后的张量,所以也没必要对想象出高阶张量那么执着。
参考文章:http://t.csdnimg.cn/OjOqS
2.2 图片与张量的相互转换:
首先,我们来思考一种最简单的情况。假如有一张灰度图像,显然它是一个二维的平面,它由密密麻麻的像素组成。对于每一个像素点如果我们用由低到高的数字来代表像素点灰色的深度,那么显然,这张图片可以被一个二阶张量所表示;
对于一张彩色图像,实际上每一个像素点的颜色是由“红、绿、蓝”(RGB)三种颜色组合而成。所以,在我们的肉眼看来,一张彩色图像是一个二维平面。但实际上,它是由一张红色图片、一张绿色图片、一张蓝色图片叠加而成的。仿照前面对一张灰度图像用二阶张量来表示,一张彩色就可以用一个三阶张量来表示。
以上只是这个转换过程在我们大脑中的想象,实际通过代码来实现这个过程我们需要借助Pytorch中的torchvision库来实现。
三、实战环节:
1 数据预处理:
当我们进行数据预处理时,归一化是一种常见的操作。它的目的是将数据调整到一个特定的范围或分布,通常是为了让模型更容易学习并且提高训练的稳定性和效率。
归一化的过程就好像把数据放进一个标准的框架中,使得它们的分布更加均匀、方便处理。最常见的归一化方法之一是将数据缩放到 0 到 1 的范围内。举个例子,如果你有一组身高数据,其中最小值是 150 厘米,最大值是 190 厘米,那么归一化之后,150 厘米的数据会变成 0,190 厘米的数据会变成 1,其他的数据会按比例变化到 0 和 1 之间。
而对于图像数据,我们归一化处理的其实是每一个像素点。例如,如果你有一组图像数据,像素值范围在 0 到 255 之间,那么归一化可以将像素值除以 255,将其缩放到 0 到 1 的范围内。
除了将数据缩放到 0 到 1 的范围外,还有其他一些归一化方法,如将数据缩放到均值为 0、标准差为 1 的正态分布中。不同的归一化方法适用于不同的情况,但它们的共同目标都是使数据更易于处理和学习。
小结:
1 归一化的方法:
(1)将数据按比例缩放到0~1之间;
(2)将数据缩放为均值为0,标准差为1的正态分布中。
2 图片数据的归一化是对其每一个像素点做归一化。
代码实现:
import torchvision.transforms as transforms
transform = transforms.Compose([transforms.ToTensor(), # 将图像转换为张量transforms.Normalize((0,), (1,)) # 归一化,均值为0,标准差为1
])
2 数据加载
2.1 Dataset:
“torchvision.datasets”是 PyTorch 中用于加载常见视觉数据集的模块。
通常情况下,我们需要自定义一个Dataset类来加载我们自己的数据,该类需要实现__len__和__getitem__方法,分别用于返回数据集的长度和访问数据集中的元素。
但Pytorch中提供了torchvision.datasets.MNIST 类,它已经实现了 Dataset 接口,可以直接用于加载 MNIST 数据集。常用的数据集包括 MNIST、CIFAR-10、CIFAR-100、ImageNet 等。
接下来,我们以Minist数据集为例,对其数据进行加载与预处理:
MINIST
Size:
28×28 灰度手写数字图像Num:
训练集 60000 和 测试集 10000,一共70000张图片Classes:
0,1,2,3,4,5,6,7,8,9
代码实现:
import torch
from torch.utils.data import Dataset, DataLoader
# 加载训练集和测试集
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
root='./data'
: 这是指定存储数据集的根目录。在这个例子中,MNIST 数据集将被下载并存储在名为data
的文件夹中。
train=False
: 这是一个布尔值参数,用于指示加载的是否是训练集。在这里,由于我们加载的是测试集,因此将其设为False
。
download=True
: 这也是一个布尔值参数,用于指示是否下载数据集。如果数据集不存在,则将其设置为True
将自动下载数据集。一旦下载完成,就会被保存在指定的root
目录中。
transform=transform
: 这是数据转换的参数,用于将加载的图像数据转换为模型可接受的张量格式,并进行归一化处理。在这里,你将之前定义的transform
应用于加载的测试集数据。
Dataset类负责从数据源中加载数据,并对数据进行预处理(如归一化、数据增强等),但它并不负责将数据组织成批次或进行随机化
2.2 Dataloader:
Dataset负责表示数据集和进行数据预处理,而DataLoader负责将数据组织成批次并加载到模型中进行训练。它接收一个“Dataset”对象作为参数,并根据指定的批量大小、是否打乱数据等参数,在实际使用中,通常会将自定义的Dataset对象传递给DataLoader进行数据加载和批处理(用上文提到的类的继承操作)。
代码实现:
import torch
from torch.utils.data import Dataset, DataLoader
# 创建数据加载器
batch_size = 4
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True,num_workers=0)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=0)
batch_size
: 这是指定每个批次(batch)包含的样本数量。在这个例子中,将每个批次设置为包含 4 个样本。
shuffle=False
: 这是一个布尔值参数,用于指示是否对数据进行洗牌操作。在测试集中,通常不需要对数据进行洗牌,因此将其设置为False
。
num_workers=0
: 这是用于数据加载的子进程数量。设置为0
表示所有数据加载操作都在主进程中进行,没有额外的子进程。在一般情况下,将其设置为大于0
的值可以加速数据加载,但在某些环境中可能会出现问题,因此这里将其设置为0
,表示只使用主进程进行数据加载操作。
3 构建神经网络模型:
# 构建神经网络模型
class CNN(nn.Module):def __init__(self):super(CNN, self).__init__()self.flatten = nn.Flatten()self.feature_extraction = nn.Sequential(nn.Linear(28*28, 16*16),nn.ReLU(),nn.Linear(16*16, 8*8),nn.ReLU(),nn.Linear(8*8, 10),nn.ReLU())# 下面定义x的传播路线def forward(self, x):x = self.flatten(x)output = self.feature_extraction(x)return output
在神经网络的构建代码中,注意到输入是28×28 的灰度手写数字图像,输出是0~9的十个数字,经过了三层全连接层来一步步提取特征。
在前行传播代码中,( x ) 是一个形参,表示输入数据。当模型进行训练或推理时,需要将实际的数据传递给模型,这个数据就会被赋值给 ( x )。
4 损失函数与优化器:
# 定义损失函数和优化器
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
上例中使用交叉熵损失函数和 SGD 优化器,都是一些固定的格式。不过要注意一 点就是这个学习率,lr=1e-3 是学习率的设置,控制着每次参数更新的步长大小。 学习率是优化算法中一个重要的超参数,它决定了模型在参数空间中搜索的速度 和精度。 例如,lr=1e-3 表示学习率为 0.001,即每次参数更新时,参数值会按照梯度 的方向移动一个较小的步长。
5 定义训练函数与测试函数:
# 定义训练函数
def train(train_dataloader1, model1, loss_fn1, optimizer1):for batch, (X, y) in enumerate(train_dataloader1):X, y = X.to(device), y.to(device)pred = model1(X)loss = loss_fn1(pred, y)optimizer1.zero_grad()loss.backward()optimizer1.step()if batch % 100 == 0:size = len(train_dataloader1)loss, current = loss.item(), batch * len(X)print(f"loss: {loss:.7f} [{current:>5d}/{size:>5d}]")# 定义测试函数
def test(test_dataloader2, model2):size = len(test_dataloader2.dataset)model2.eval()test_loss, correct = 0, 0with torch.no_grad():for X, y in test_dataloader2:X, y = X.to(device), y.to(device)pred = model(X)test_loss += loss_fn(pred, y).item()correct += (pred.argmax(1) == y).type(torch.float).sum().item()test_loss /= sizecorrect /= sizeprint(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
6 主程序:
# 主程序
epochs = 5
for t in range(epochs):print(f"Epoch {t+1}\n")train(train_dataloader, model, loss_fn, optimizer)test(test_dataloader, model)
6 模型的保存与评估:
# 保存模型
torch.save(model.state_dict(), "./model")
print("模型保存在model文件夹内")# 加载模型
model = CNN()
model.load_state_dict(torch.load("./model"))
model.eval() # 将模型设置为评估模式
当调用model.eval()时,就像正在告诉模型:“现在我们要用你来做测试或者预测了,所以请你把自己整理好,别再做什么让结果不确定的事情了。”就好像是在考试前整理心情一样,你希望自己在考试时保持冷静和准确,不要因为紧张或者分心而出错。
四、完整代码:
# 导入库
import torch
import torchvision
# 名称简化
import torch.nn as nn
import torchvision.transforms as transforms
from torch.utils.data import DataLoader# 下载手写数字数据库
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])train_dataset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_dataloader = DataLoader(train_dataset, batch_size=4, shuffle=True, num_workers=0)test_dataset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)
test_dataloader = DataLoader(test_dataset, batch_size=4, shuffle=False, num_workers=0)# 构建神经网络模型
class CNN(nn.Module):def __init__(self):super(CNN, self).__init__()self.flatten = nn.Flatten()self.feature_extraction = nn.Sequential(nn.Linear(28*28, 16*16),nn.ReLU(),nn.Linear(16*16, 8*8),nn.ReLU(),nn.Linear(8*8, 10),nn.ReLU())# 下面定义x的传播路线def forward(self, x):x = self.flatten(x)output = self.feature_extraction(x)return outputdevice = torch.device('cuda') # 将device指定为GPU
model = CNN().to(device) # 将模型移动到GPU上
print(model)# 定义损失函数和优化器
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)# 定义训练函数
def train(train_dataloader1, model1, loss_fn1, optimizer1):for batch, (X, y) in enumerate(train_dataloader1):X, y = X.to(device), y.to(device)pred = model1(X)loss = loss_fn1(pred, y)optimizer1.zero_grad()loss.backward()optimizer1.step()if batch % 100 == 0:size = len(train_dataloader1)loss, current = loss.item(), batch * len(X)print(f"loss: {loss:.7f} [{current:>5d}/{size:>5d}]")# 定义测试函数
def test(test_dataloader2, model2):size = len(test_dataloader2.dataset)model2.eval()test_loss, correct = 0, 0with torch.no_grad():for X, y in test_dataloader2:X, y = X.to(device), y.to(device)pred = model(X)test_loss += loss_fn(pred, y).item()correct += (pred.argmax(1) == y).type(torch.float).sum().item()test_loss /= sizecorrect /= sizeprint(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")# 主程序
epochs = 5
for t in range(epochs):print(f"Epoch {t+1}\n")train(train_dataloader, model, loss_fn, optimizer)test(test_dataloader, model)# 保存和恢复网络权值
torch.save(model.state_dict(), "./model")
print("模型保存在model文件夹内")# 加载模型
model = CNN()
model.load_state_dict(torch.load("./model"))
model.eval() # 将模型设置为评估模式