基本概念

image.png
机器学习开发的步骤:
1.数据处理:主要包括数据清理、数据预处理、数据增强等,就是构建让模型使用的训练集与测试集。
2.模型训练:确定网络结构、损失函数、优化方法。
3.模型评估:使用各种评估指标来评估模型的好坏。
其中,模型训练部分又分为以下几步:
1.确定网络结构:机器学习中回归算法、SVM等,深度学习中的VGG、ResNet、SENet等各种网络结构。
2.确定损失函数:损失函数衡量的是当前模型预测结果与真实标签之间的差距。
3.确定优化方法:与损失函数搭配,更新模型中的参数。

环境搭建

  1. 安装python和pip
  2. 安装pytorch:pip install torch
    1. 验证pytorch:在terminal中输入python,进入repl,输入import torch,回车没有报错就说明已经装好了
  3. 安装jupyter notebook:pip install jupyter
    1. 启动:jupyter notebook

      python基本操作

      | API | 用途 | | | —- | —- | —- | | type() | 获取变量的类型 | | | len() | 获取列表长度 | | | display() | 显示图片 | |

numpy

数组库。
安装:pip install numpy
numpy数组的特点:

  1. 长度不可变
  2. 元素的类型必须一致
  3. 计算速度快,内存占用少

以下的API对numpy做了别名:import numpy as np

API 用途 参数 示例
np.asarray() 创建数组,浅拷贝 数组, [数据类型] np.asarray([[1, 2], [3, 4]])
np.array() 创建数组,深拷贝 数组, [数据类型] np.array([[1, 2], [3, 4]])
np.ones() 创建数组,元素全为1 形状 np.ones(shape=(2,3))
np.zeros() 创建数组,元素全为0 形状 np.zeros(shape=(2,3))
np.arange() 创建数组,元素是一个序列 [start, ]stop, [step, ] np.arange(5) # array([0, 1, 2, 3, 4])
np.linspace() 创建数组,元素是一个序列 start, stop, num=50 np.linspace(start=2, stop=10, num=3) # array([ 2., 6., 10.])
a.view() 浅拷贝
a.copy() 深拷贝
a.ndim 数组的维度
a.ndim # 2
a.shape 数组的形状,即各维度上的长度
a.shape # (2, 2)
a.reshape() 改变数组的形状 表示形状的元组 a.reshape((4, 1))
a.size 数组中元素的个数
a.dtype 数组中元素的类型,⚠️不要设置这个属性 a.dtype # dtype(‘int64’)
a.astype() 返回一个改变了元素数据类型后的新数组 元素数据类型 a.astype(‘int32’)
a.sum() 求和 数组, 轴 np.sum(arr_2d, axis=0) # [2, 3, 4]
a.mean() 求平均值 arr_2d.mean(axis=0) # [2, 3, 4]
a.max() 求最大值
a.min() 求最小值
a.argmax() 求最大值下标
a.argmin() 求最小值下标
np.argsort() 排序,返回排序后的索引数组 数组,轴
a[:, :, 0] 索引(降维)
a = a[:, :, np.newaxis] 升维
a[:, :, 0:1] 切片
np.concatenate() 拼接,维度不变 (a1, a2, …), axis=0

pillow

图片处理库。
pillow提供了广泛的文件格式支持,强大的图像处理能力,主要包括图像储存、图像显示、格式转换以及基本的图像处理操作等。
安装:pip install pillow

  1. from PIL import Image
  2. im = Image.open('jk.jpg')
  3. im.size # (318, 116)

提取图片的RGB通道

# pillow方式读取图片
from PIL import Image

im = Image.open('jk.webp') # 读取图片文件

import numpy as np

im_pillow = np.asarray(im) # 转换成numpy类型

im_pillow_c1 = im_pillow[:, :, 0] # 提取RGB通道
im_pillow_c2 = im_pillow[:, :, 1]
im_pillow_c3 = im_pillow[:, :, 2]

zeros = np.zeros((im_pillow.shape[0], im_pillow.shape[1], 2)) # 构建单通道图片

im_pillow_c1 = im_pillow_c1[:, :, np.newaxis] # 将2维数组升成3维数组

im_pillow_c1_3ch = np.concatenate((im_pillow_c1, zeros), axis=2) # 拼接R通道

im_pillow_c2_3ch = np.zeros(im_pillow.shape) # G、B通道使用赋值的方式构建
im_pillow_c2_3ch[:,:,1] = im_pillow_c2

im_pillow_c3_3ch = np.zeros(im_pillow.shape)
im_pillow_c3_3ch[:,:,2] = im_pillow_c3

# 展示通道
from matplotlib import pyplot as plt
plt.subplot(2, 2, 1)
plt.title('Origin Image')
plt.imshow(im_pillow)
plt.axis('off')
plt.subplot(2, 2, 2)
plt.title('Red Channel')
plt.imshow(im_pillow_c1_3ch.astype(np.uint8))
plt.axis('off')
plt.subplot(2, 2, 3)
plt.title('Green Channel')
plt.imshow(im_pillow_c2_3ch.astype(np.uint8))
plt.axis('off')
plt.subplot(2, 2, 4)
plt.title('Blue Channel')
plt.imshow(im_pillow_c3_3ch.astype(np.uint8))
plt.axis('off')
plt.savefig('./rgb_pillow.png', dpi=150)

image.png

OpenCV

图片处理库。
演示如何提取图片的RGB通道:

# cv2方式读取图片
import cv2
im_cv2 = cv2.imread('jk.webp') # 读取图片文件,直接得到numpy类型,不需要转换

# 反转通道顺序,cv2读取图片后通道顺序是BGR,在这里逆转一下顺序
im_cv2_copy = im_cv2.copy() # 深拷贝
im_cv2_copy[:, :, 0] = im_cv2[:, :, 2]
im_cv2_copy[:, :, 2] = im_cv2[:, :, 0]
im_cv2 = im_cv2_copy

# 提取通道
im_cv2_c1_3ch = im_cv2.copy()
im_cv2_c1_3ch[:, :, 1:] = 0
im_cv2_c2_3ch = im_cv2.copy()
im_cv2_c2_3ch[:, :, 0] = 0
im_cv2_c2_3ch[:, :, 2] = 0
im_cv2_c3_3ch = im_cv2.copy()
im_cv2_c3_3ch[:, :, :1] = 0

# 展示通道
from matplotlib import pyplot as plt
plt.subplot(2, 2, 1)
plt.title('Origin Image')
plt.imshow(im_cv2)
plt.axis('off')
plt.subplot(2, 2, 2)
plt.title('Red Channel')
plt.imshow(im_cv2_c1_3ch.astype(np.uint8))
plt.axis('off')
plt.subplot(2, 2, 3)
plt.title('Green Channel')
plt.imshow(im_cv2_c2_3ch.astype(np.uint8))
plt.axis('off')
plt.subplot(2, 2, 4)
plt.title('Blue Channel')
plt.imshow(im_cv2_c3_3ch.astype(np.uint8))
plt.axis('off')
plt.savefig('./rgb_cv2.png', dpi=150)

image.png

Tensor

张量,是包含了标量、向量、矩阵等等的一种数据结构,并提供了GPU加速能力

API 用途 参数
torch.tensor() 创建tensor
不要写成torch.Tensor()
data:标量、元组、数组、np数组
dtype=None
device=None
requires_grad=False
torch.from_numpy() 创建tensor,从np数组
torch.zeros() 创建tensor,全0 *size, dtype=None…
torch.ones() 创建tensor,全1
torch.eye() 创建tensor,单位矩阵
torch.rand() 创建随机tensor,0-1均匀分布 size
torch.randint() 创建随机tensor,x-y均匀分布 low, high, size
torch.randn() 创建随机tensor,(0, 1)正态分布 size
torch.normal() 创建随机tensor,(x, y)正态分布 mean, std, size
a.item() torch => 标量
a.numpy() torch => np
a.numpy().tolist() torch => 数组
a.shape 获取tensor的形状
a.size() 获取tensor的形状
a.numel() 获取tensor的元素个数
a.permute() 返回转置后的tensor,所有维度 形状下标
a.transpose() 返回转置后的tensor,两个维度 形状下标
a.view() 返回变形后的tensor,必须连续 形状
a.reshape() 返回变形后的tensor,不必连续 形状
a.squeeze() 降维,删除一个长度为1的维度 形状下标
a.unsqueeze() 升维,插入一个长度为1的维度 形状下标
torch.cat() 合并,维度不变 (a1, a2…), 轴
torch.stack() 合并,升维 (a1, a2…), 轴
torch.chunk() 拆分,维度不变 a, 分几块, 轴
torch.split() 拆分,维度不变 a, 每块长, 轴
torch.unbind() 拆分,降维 a, 轴
a[:, 1] 索引,降维
a[:, 0:2] 切片,维度不变
torch.index_select() 过滤,维度不变 tensor, dim, index
torch.masked_select() 过滤,返回一维数组 a, mask

Torchvision

与pytorch配合使用的图片处理库,包括了常用数据集+常见网络模型+常用图像处理方法。Torchvision依赖pillow进行图片处理。
安装:pip install torchvision

Dataset抽象类

pytorch中的Dataset抽象类是对数据集的抽象

import torch
from torch.utils.data import Dataset

# 实现一个自定义的数据集类,继承Dataset
class MyDataset(Dataset):
    # 构造函数
    def __init__(self, data_tensor, target_tensor):
        self.data_tensor = data_tensor
        self.target_tensor = target_tensor
    # 返回数据集大小,配合全局len()使用
    def __len__(self):
        return self.data_tensor.size(0)
    # 返回索引的数据与标签,配合索引语法使用
    def __getitem__(self, index):
        return self.data_tensor[index], self.target_tensor[index]

# 生成数据
data_tensor = torch.randn(10, 3)
target_tensor = torch.randint(2, (10,)) # 标签是0或1

# 将数据封装成Dataset
my_dataset = MyDataset(data_tensor, target_tensor)

# 查看数据集大小
print('Dataset size:', len(my_dataset)) # Dataset size: 10

# 使用索引调用数据
print('tensor_data[0]: ', my_dataset[0])

DataLoader类

pytorch中的DataLoader类用于将Dataset中包含的数据加载到内存中,能够多进程、迭代地加载数据。本身是一个迭代器。

# DataLoader的用法
from torch.utils.data import DataLoader
tensor_dataloader = DataLoader(dataset=my_dataset, # 传入的数据集
                               batch_size=2,       # 每次取几条数据
                               shuffle=True,       # 数据是否打乱
                               num_workers=0)      # worker进程数, 0表示只有主进程

# 以循环形式输出
for data, target in tensor_dataloader: 
    print(data, target)

# 输出一个batch,可以无限次调用
print('One batch tensor data: ', iter(tensor_dataloader).next())

数据读取

torchvision.datasets包提供的所有数据集参见:Datasets — Torchvision 0.12 documentation
这些数据集都是Dataset类的子类,可以被DataLoader处理。
MNIST是一个手写数字数据集,常用来进行深度学习的入门。
image.png
image.png
使用数据集api,完成对数据的下载、解压,返回值是Dataset类的子类实例。
使用torchvision.datasets的其他方法,即可下载其他数据集。
如果要使用官方数据集以外的自定义数据集,可以使用torchvision.datasets.ImageFolder

import torchvision
mnist_dataset = torchvision.datasets.MNIST(root='./data', # 文件保存目录
                                       train=False, # 加载训练集 or 测试集
                                       transform=None, # 图片预处理
                                       target_transform=None, # 标签预处理
                                       download=True) # 是否下载

mnist_dataset_list = list(mnist_dataset) # 将数据集转换成list
print(mnist_dataset_list) # [(图片数据, 标签)]

display(mnist_dataset_list[0][0]) # 显示第一张图片
print("Image label is:", mnist_dataset_list[0][1]) # 第一张图片的标签 7

image.png

数据转换

此时已经完成了图片的读取,但得到的是图片类型,接下来需要转换成tensor类型才能用于计算。
torchvision.transforms提供了类型转换的能力:

  • transforms.ToTensor():图片 => tensor
  • transforms.ToPILImage():tensor => 图片

还可以对图片和tensor进行以下处理:

  • transforms.Resize():变形
  • transforms.CenterCrop():中心剪裁
  • transforms.RandomCrop():随机剪裁
  • transforms.FiveCrop():中心和四角剪裁
  • transforms.RandomHorizontalFlip(p=0.5):以概率p随机水平翻转
  • transforms.RandomVerticalFlip(p=0.5):以概率p随机垂直翻转

只针对tensor的操作

  • transforms.Normalize():归一化,减去通道平均值,再除以标准差。

以上的操作还可以串联起来:

  • transforms.Compose(),组合后的操作可以传给datasets.MNIST等方法,在数据读取时就可以一并完成转换

    网络模型

    torchvision包含的网络模型可以完成四类任务:图像分类、物体检测、图像分割和视频分类
    image.png
    GoogLeNet是一个Google推出的基于Inception模块的深度神经网络模型。所有模型参见pytorch文档
    import torchvision.models as models
    # 初始化一个模型,参数都是随机的,需要训练后才能使用
    googlenet = models.googlenet()
    # 或者使用ImageNet预训练的模型,可以直接使用;初次调用会下载模型参数
    # 下载位置由TORCH_MODEL_ZOO来指定
    googlenet = models.googlenet(pretrained=True)
    
    使用已经预训练的模型,再用自己的数据集进行微调,只训练最后的全连接层,可以快速得到一个效果较好的模型。这样比从头训练一个随机初始化的模型,速度更快而且效果更好。
    预训练的googlenet模型是基于ImageNet数据集的,它有1000个分类。如果我们自己的任务只有10个分类,可以这样做: ```python import torch import torchvision.models as models

加载预训练模型

googlenet = models.googlenet(pretrained=True)

提取分类层的输入参数

fc_in_features = googlenet.fc.in_features print(“fc_in_features:”, fc_in_features) # fc_in_features: 1024

查看分类层的输出参数

fc_out_features = googlenet.fc.out_features print(“fc_out_features:”, fc_out_features) # fc_out_features: 1000

修改预训练模型的输出分类数

googlenet.fc = torch.nn.Linear(fc_in_features, 10)

torchvision.utils.make_grid()可以将图片拼接成网格,便于查看数据集
```python
import torchvision
from torchvision import datasets
from torchvision import transforms
from torch.utils.data import DataLoader

# 加载MNIST数据集
mnist_dataset = datasets.MNIST(root='./data',
                               train=False,
                               transform=transforms.ToTensor(),
                               target_transform=None,
                               download=True)
# 取32张图片的tensor
tensor_dataloader = DataLoader(dataset=mnist_dataset,
                               batch_size=32)
data_iter = iter(tensor_dataloader)
img_tensor, label_tensor = data_iter.next()
print(img_tensor.shape) # torch.Size([32, 1, 28, 28]) [batch大小,通道数,高,宽]

# 将32张图片拼接在一个网格中
grid_tensor = torchvision.utils.make_grid(img_tensor, nrow=8, padding=2)
print(grid_tensor.shape) # torch.Size([3, 122, 242])
grid_img = transforms.ToPILImage()(grid_tensor)
display(grid_img)

image.png
torchvision.utils.save_image()可以将tensor保存为图片文件,省去了将tensor转换成图片数据的步骤

# 输入为一张图片的tensor,直接保存,结果同上面的图片
torchvision.utils.save_image(grid_tensor, 'grid.jpg')

# 输入为List,调用grid_img函数后保存,结果如下图
torchvision.utils.save_image(img_tensor, 'grid2.jpg', nrow=5, padding=2)

grid2.jpg

卷积

卷积用于对图片进行特征提取。卷积具有两个特点:

  • 稀疏连接:使参数的数量变得很少
  • 平移不变性:图形的位置不影响计算结果

卷积的计算过程是,将输入特征图与卷积核做卷积计算(逐个相乘再相加),得到输出特征图中的一个元素。然后向右、向下移动计算区域,就可得出输出特征图中的每个元素。在下图的例子中,每次移动的步长(stride)为1,步长一般是1或2。
image.png

标准卷积

上面是最简单的情况,扩展到标准情况下后,输入特征图有m个通道,卷积核也有m个通道;每个卷积核在卷积计算后得到输出特征图的一个通道,n个卷积核就得到了输出特征图的n个通道:
image.png
卷积操作会让特征图越来越小,为了避免或者减慢这种特征图变小的过程,可以做padding操作,就是在结果的周围补几圈0,或者在输入特征图中右侧和下方补0,使输出和输入一致。

import torch
import torch.nn as nn

# 输入特征图
# shape = (batch_size,通道数,高,宽)
a = [[4, 1, 7, 5], [4, 4, 2, 5], [7, 7, 2, 4], [1, 0, 2, 4]]
input = torch.tensor(a, dtype=torch.float32).unsqueeze(0).unsqueeze(0)

# 卷积操作
# 输入通道数1,输出通道数1,卷积核大小(2, 2),步长1,补0,加偏移
conv2d = nn.Conv2d(1, 1, (2, 2), stride=1, padding='same', bias=True)

# 输出特征图
# shape = (batch_size,通道数,高,宽)
output = conv2d(input)

深度可分离卷积

深度可分离卷积(Depthwise Separable Convolution)体积小、速度快、精度稍低,适合用在移动端。
第一步是Depthwise(DW)卷积
image.png
第二步是Pointwise(PW)卷积
image.png
使用深度可分离卷积的话,计算量只有标准卷积的1/k^2
通过nn.Conv2d的groups参数可以实现DW,即对输入特征图进行分组卷积,groups即为通道数

import torch
import torch.nn as nn

# 生成一个3通道的5*5特征图
input_feat = torch.rand((3, 5, 5)).unsqueeze(0)
print(input_feat.shape) # torch.Size([1, 3, 5, 5])

# 请注意DW中,输入特征通道数与输出通道数是一样的
in_channels_dw = input_feat.shape[1]
out_channels_dw = input_feat.shape[1]

# 一般来讲DW卷积的kernel_size为3
kernel_size = 3

# DW卷积groups参数与输入通道数一样
dw = nn.Conv2d(
    in_channels_dw,
    out_channels_dw,
    kernel_size,
    groups=in_channels_dw)

# 然后创建PW卷积
in_channels_pw = out_channels_dw
out_channels_pw = 4
kernel_size_pw = 1
pw = nn.Conv2d(in_channels_pw, out_channels_pw, kernel_size_pw, stride)
output = pw(dw(input_feat))
print(output.shape) # torch.Size([1, 4, 3, 3])

空洞卷积

既保证有比较大的感受野,同时又不用缩小特征图,用于图像分割任务,动图参见github
通过nn.Conv2d的dilation参数来实现空洞卷积。
image.pngimage.png

损失函数

单个样本点上,模型预测值和真实值之间的误差,称为损失函数。
模型学习的过程,就是让损失函数值不断减少的过程。
以平方损失函数为例:
image.png
所有样本点的误差平均值,称为代价函数。
image.png
设真实值为F(x),模型预测值为f(x),一些常用的损失函数有:

  • 0-1损失函数:预测值与真实值相等时为0,不相等时为1

image.png

  • 平方损失函数

image.png

  • 均方差损失函数(Mean Squared Error,MSE)

image.png
s为目标值的向量表示,y为预测值的向量表示。

  • 平均绝对误差损失函数(Mean Absolute Error,MAE)

image.png

  • 交叉熵损失函数(Cross entropy loss)

image.png

  • softmax函数:用于将输入映射到0-1之间

image.png

  • sigmoid函数

    梯度、前向传播、反向传播

    前向传播:输入数据逐层、单向地经过各层,最终达到输出层。
    导数:
    image.png
    偏导数:
    image.png
    梯度,即函数所有偏导数构成的向量:
    image.png
    梯度向量的方向即为函数值增长最快的方向,所以模型学习的过程就是沿着损失函数的梯度的相反方向进行,即损失函数减少最快的方向,最终达到损失函数的最小值。
    我们的目标是计算损失函数的梯度:
    image.png
    wij表示第i层的第j个节点对应的权重值
    但损失函数的表达式可能很复杂,为了简化求导运算,我们需要借助链式法则:两个函数组合起来的复合函数,导数等于内层函数代入外层函数值的导数,乘以内层函数的导数。可表述为:
    image.png

    image.png
    例如,对于
    image.png
    可以分解为 f(x)=cos (x) 和 g(x)=x2-1
    其中, f(x)=cos (x) 的导数是 f(x)=-sin(x),g(x)=x2-1 的导数是 g(x) = 2x,所以原函数的导数是:
    image.png
    反向传播算法(Backpropagation)是目前训练神经网络最常用且最有效的算法。其基本过程是:
  1. 前向传播:数据逐步经过输入层、隐藏层、输出层。
  2. 计算误差并传播:计算模型输出结果和真实结果之间的误差,并将这种误差通过某种方式反向传播,即从输出层向隐藏层传递并最后到达输入层。
  3. 迭代:在反向传播的过程中,根据误差不断地调整模型的参数值,并不断地迭代前面两个步骤,直到达到模型结束训练的条件。

    优化方法

    优化方法:将误差反向传播并调整模型参数,使模型效果达到最优的过程。
    主要有三种优化方法:

  4. 批量梯度下降法(Batch Gradient Descent,BGD),使用训练集的全部数据,效率较低

  5. 随机梯度下降(Stochastic Gradient Descent,SGD),每次使用一条数据,容易陷于局部最优
  6. 小批量梯度下降(Mini-Batch Gradient Descent, MBGD),对前两者的折中,最常用

    模型搭建和训练

    下面以线性回归为例,演示模型的搭建和训练过程 ```python

    首先构建训练用的数据

    import numpy as np import random from matplotlib import pyplot as plt

w = 2 # weight b = 3 # bias xlim = [-10, 10] x_train = np.random.randint(low=xlim[0], high=xlim[1], size=30) # 训练集

y_train = [w * x + b + random.randint(0,2) for x in x_train] # 标签

plt.plot(x_train, y_train, ‘bo’)

![image.png](https://cdn.nlark.com/yuque/0/2022/png/1591393/1651685040153-064571ba-d170-4bff-9634-95862813fae1.png#clientId=u26e349b3-8d59-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=254&id=u63745822&margin=%5Bobject%20Object%5D&name=image.png&originHeight=508&originWidth=762&originalType=binary&ratio=1&rotation=0&showTitle=false&size=39544&status=done&style=none&taskId=u42ba930d-6a50-4bb9-b070-1bb4e4e69f9&title=&width=381)
```python
import torch
from torch import nn

# 定义模型,需要继承nn.Module类
class LinearModel(nn.Module):
  # 构造函数,需要学习参数的层要放在这里
  def __init__(self):
    super().__init__() # 必须先调用父类的构造方法
    self.weight = nn.Parameter(torch.randn(1)) # nn.Parameter()返回可训练的参数
    self.bias = nn.Parameter(torch.randn(1))

  # 前向传播,提供输出;该类的实例是一个函数,被调用时就会调用forward()
  def forward(self, input): 
    return (input * self.weight) + self.bias

# 实例化模型
model = LinearModel()
# 优化方法选择SGD
optimizer = torch.optim.SGD(
    model.parameters(),
    lr=1e-4,
    weight_decay=1e-2,
    momentum=0.9
)

y_train = torch.tensor(y_train, dtype=torch.float32)

# 训练1000个Epoch
for _ in range(1000):
    input = torch.from_numpy(x_train)
    output = model(input)
    loss = nn.MSELoss()(output, y_train) # 损失函数使用MSE
    model.zero_grad()
    loss.backward()
    optimizer.step()

for parameter in model.named_parameters():
  print(parameter)
# 输出:
# ('weight', Parameter containing:
# tensor([1.9803], requires_grad=True))
# ('bias', Parameter containing:
# tensor([3.4471], requires_grad=True))

至此就完成了模型的训练,得出了模型的参数。
模型中反复出现的部分,还可以提取成子模型,供其他模型调用。
训练好的模型可以保存起来,供以后使用。我们可以只保存参数:

# 保存模型参数到一个pth文件
torch.save(model.state_dict(), './linear_model.pth')

# 重新实例化一个模型
linear_model = LinearModel()

# 加载保存的参数
linear_model.load_state_dict(torch.load('./linear_model.pth'))
linear_model.eval() # 使模型进入测试状态
for parameter in linear_model.named_parameters():
  print(parameter)
# 输出:
# ('weight', Parameter containing:
# tensor([1.9803], requires_grad=True))
# ('bias', Parameter containing:
# tensor([3.4471], requires_grad=True))

还可以把模型和参数都保存起来:

# 保存整个模型
torch.save(model, './linear_model_with_arc.pth')

# 加载模型,不需要创建网络了
linear_model_2 = torch.load('./linear_model_with_arc.pth')
linear_model_2.eval()
for parameter in linear_model_2.named_parameters():
  print(parameter)
# 输出:
# ('weight', Parameter containing:
# tensor([1.9803], requires_grad=True))
# ('bias', Parameter containing:
# tensor([3.4471], requires_grad=True))

在实际开发中,基本上不需要自己搭建网络,一般都是使用torchvision内置的网络,在其基础上做微调来加快收敛速度。这里使用的微调方式是,修改全连接层输出的数目,使用预训练模型的参数作为初始化参数,重新训练整个网络。以AlexNet为例:

# 首先,导入网络
import torchvision.models as models
alexnet = models.alexnet(pretrained=True)

# 然后,验证模型是否正确加载
from PIL import Image
import torchvision
import torchvision.transforms as transforms

im = Image.open('corgi.jpg') # 一张柯基的图片

transform = transforms.Compose([
    transforms.RandomResizedCrop((224,224)),
    transforms.ToTensor(),
    # imageNet要求的标准化方式
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
  ])

input_tensor = transform(im).unsqueeze(0) # imageNet要求的形状,增加了batch的维度
alexnet(input_tensor).argmax() # 263,对应柯基的标签

输出263说明模型已经正确加载,下面来做微调。torchvision内置的模型都是基于imageNet训练的,有1000个类别,我们现在使用的alexnet同样也是。如果我们想将这个模型应用在CIFAR-10数据集上(只有10个类别),那么我们需要将模型最后的全连接层输出修改为10,并重新训练模型:

print(alexnet) # 最后的全连接层输出为1000个单元
# AlexNet(
# ...
# (classifier): Sequential(
#   ...    
#   (6): Linear(in_features=4096, out_features=1000, bias=True)
# ...

# 提取分类层的输入参数
fc_in_features = alexnet.classifier[6].in_features

# 修改预训练模型的输出分类数为10
alexnet.classifier[6] = torch.nn.Linear(fc_in_features, 10)

# 加载CIFAR-10数据
transform = transforms.Compose([
    transforms.RandomResizedCrop((224,224)), # torchvision最小训练尺寸
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    ])

cifar10_dataset = torchvision.datasets.CIFAR10(root='./data',
                                       train=False,
                                       transform=transform,
                                       target_transform=None,
                                       download=True)

dataloader = DataLoader(
    dataset=cifar10_dataset, # 传入的数据集, 必须参数
    batch_size=32, # 输出的batch大小
    shuffle=True,  # 数据是否打乱
    num_workers=2  # worker进程数, 0表示只有主进程
)      

# 优化器
optimizer = torch.optim.SGD(
    alexnet.parameters(),
    lr=1e-4,
    weight_decay=1e-2,
    momentum=0.9
)

# 训练3个Epoch
for epoch in range(3):
    for item in dataloader: 
        output = alexnet(item[0]) # 预测值
        target = item[1]          # 真实值
        loss = nn.CrossEntropyLoss()(output, target) # 使用交叉熵损失函数
        print('Epoch {}, Loss {}'.format(epoch + 1 , loss))
        alexnet.zero_grad()
        loss.backward()
        optimizer.step()

# Epoch 1, Loss 2.3692712783813477
# Epoch 1, Loss 2.2741799354553223
# ...
# Epoch 1, Loss 1.3783353567123413
# Epoch 2, Loss 1.33861243724823
# ...
# Epoch 2, Loss 0.5767470598220825
# Epoch 3, Loss 1.2658960819244385
# ...
# Epoch 3, Loss 1.0122954845428467 结束

训练过程可视化监控

通过可视化可以在模型训练的过程中监控参数、评价指标等。下面介绍Tensorboard和Visdom

Tensorboard

Tensorboard是TensorFlow内置的可视化工具,为了与pytorch配合使用,我们需要TensorboardX的帮助。pytorch 1.8版本之后自带TensorboardX,无需安装。

# 安装tensorboard
pip install tensorboard

使用Tensorboard时,首先将训练过程中产生的数据写到日志文件中,然后再从浏览器页面读取并绘制这些数据。(数据有更新时,只能手动刷新页面么?)
首先将训练过程的数据写到日志文件中:

from torch.utils.tensorboard import SummaryWriter
import numpy as np

# 创建一个SummaryWriter的实例
# 负责在日志中写入数值或图像
# 可选参数是日志保存的位置,默认./runs/当前时间_主机名
writer = SummaryWriter()

# 写入数据
for step in range(100):
    # 参数为数据名称,数值,步数
    writer.add_scalar('Loss/train', np.random.random(), step)
    writer.add_scalar('Loss/test', np.random.random(), step)
    writer.add_scalar('Accuracy/train', np.random.random(), step)
    writer.add_scalar('Accuracy/test', np.random.random(), step)

# 构建一张图片
img = np.zeros((3, 100, 100))
img[0] = np.arange(0, 10000).reshape(100, 100) / 10000
img[1] = 1 - np.arange(0, 10000).reshape(100, 100) / 10000

# 写入图片,参数为数据名称,数值,步数
writer.add_image('my_image', img, 0)
writer.close()

然后在终端启动Tensorboard:
tensorboard —logdir=runs
最后在浏览器中打开http://localhost:6006,就可以看到刚才写入的数据和图片了。
image.png
image.png
前面的alexnet微调的例子可以得到下面的收敛过程:(2个epoch)
image.png

Visdom

Visdom是Facebook开源的一个专门用于PyTorch的交互式可视化工具。

# 安装
pip install visdom

# 运行
python3 -m visdom.server

打开http://localhost:8097,即可进入Visdom的界面。Visdom不需要Tensorboard那样先写日志文件再绘制,启动服务开始训练后直接就可以看到图形,而且当数据更新时页面会自动更新👍

# visdom
from visdom import Visdom
import numpy as np
import time

# 1. 将窗口类实例化
viz = Visdom()

# 2. 创建窗口并初始化,创建折线图
viz.line([0.], [0], win='train_loss', opts=dict(title='train_loss'))

# 更新折线图
for n_iter in range(10):
    # 随机获取loss值
    loss = 0.2 * np.random.randn() + 1
    # 3. 更新窗口图像
    viz.line([loss], [n_iter], win='train_loss', update='append')
    time.sleep(0.5)

# 创建图像
img = np.zeros((3, 100, 100))
img[0] = np.arange(0, 10000).reshape(100, 100) / 10000
img[1] = 1 - np.arange(0, 10000).reshape(100, 100) / 10000

# 展示图像
viz.image(img)

image.png
对线性回归的监控:
image.png

分布式训练

分布式的目的是提高训练速度。
可以将数据和模型“分布”。
有两个相关的API:

  1. DP,即torch.nn.DataParallel:
    1. 适用于单机多卡。
    2. 单进程。
    3. 数据均分到各个GPU。
    4. 在网络前向传播时,模型会从主GPU复制到其它GPU上,但会有一个主GPU负责计算所有的loss。
    5. 在反向传播时,每个GPU上的梯度会汇总到主GPU上,求得梯度均值更新模型参数后,再复制到其它GPU,以此来实现并行。会有负载不均衡的问题。
  2. DDP,即torch.nn.DistributedDataParallel:
    1. 既能用于单机多卡,也能用于多机多卡,是更推荐的方式。
    2. 为每个GPU创建一个进程。每个GPU角色相同,不分主从。
    3. 使用分布式数据采样器(DistributedSampler)加载数据,确保数据在各个进程之间没有重叠。
    4. 反向传播时,各GPU梯度计算完成后进行广播,然后每个进程在各自的GPU上进行梯度更新。

DDP的训练过程:

  1. 初始化进程组
    • torch.distributed.init_process_group
  2. 模型并行化:将模型分发到各主机
    • torch.nn.parallel.DistributedDataParallel
  3. 创建分布式数据采样器:每个主机从硬盘上的数据集中挑选出自己负责的那部分数据
    • torch.utils.data.distributed.DistributedSampler

      图像分类

      单层感知机每个神经元输出的计算公式:
      image.png
      其中y是输出值(概率),δ是Softmax激活函数,w是权重,x是输入值,b是偏移,i是神经元下标。
      Softmax函数的作用是将输出值转换成概率值,且所有概率值之和为1,公式如下:
      image.png
      历年ImageNet图像分类比赛结果:
      image.png
  • VGG
    • 证明了随着模型深度的增加,模型效果也会越来越好。
    • 使用较小的3x3卷积核,代替了AlexNet中的11x11、7x7以及5x5的大卷积核。
  • GoogLeNet
    • 考虑到物体在图片中占比不同,使用了不同大小的卷积核来提取特征。
    • 使用了1x1卷积来降维,以降低计算成本。
  • ResNet

    • 分类成功率首次超过人类
    • 通过残差学习,使网络强制达到恒等映射,以解决网络退化问题(层数增加反而变差)。

      图像分割

      图像分割是对图像中的每一个像素点做分类,确定该点属于哪一个实体。
      图像分割任务中,由于需要对每个像素做分类,图像经过卷积层的特征提取后的特征图大小都等于或接近原图,而图像分类中特征图都很小。
      使用上采样可以将输出还原到原图大小,其中最常用的方式是转置卷积。
      损失函数可以用交叉熵损失函数、Dice Loss
      最常用的数据集是COCO
      网络模型可以使用UNet
      准备数据使用Labelme
      评价指标使用mIoU(平均交并比)

      NLP

      NLP任务可以分为两类:
  • 分类

    • 情感分析:判断文本是好评还是差评,基于LSTM网络(在RNN基础上改进而来,RNN适合处理变长的、序列相关的数据)
    • 文本分类:新闻是经济、社会、医疗还是体育,基于BERT。演进过程:统计 - 贝叶斯方法 - SVM(支持向量机) - LDA - BERT。BERT能够解决类别多、数据不平衡和多语言问题。
  • 生成
    • 摘要生成:基于BART
    • 自动问答
    • 机器翻译

分词可以使用jieba(结巴还行😭)
文本表示传统上使用One-hot,在深度学习领域则使用Word Embedding
关键词提取主要使用无监督学习,可以通过统计法、词图模型法和主题模型法来完成
注意力模型为文本中的不同部分分配了不同的注意力,来区分其重要程度。该领域最重要的论文是《Attention is All You Need》。BERT就是基于注意力模型。