PyTorch学习笔记

PyTorch学习笔记。基于《Deep Learning with PyTorch》,主要为相关语法的笔记。 用来自己写代码的时候参考。Dataset部分还需要进一步完善。

环境

import torch
import torch.optim as optim
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.data as data
from torchvision import transforms

torch.nn提供常见的神经网络结构

torch.util.data用于加载数据(使用Dataset和DataLoader)

torch.optim用于优化

torch.nn.DataParallel和torch.distributed用于特定平台的加速计算

torchvision.transforms提供常见图形格式的转换

Tensor

Basic

# 生成tensor
>>> a = torch.Tensor([[1.,2.],[2.,3.],[3.,4.]])
>>> b = torch.FloatTensor([1.,2.,3.])
# 查看相关信息
>>> print(a.shape)
torch.Size([3,2])
# tensor是按维度一次顺序储存(先行后列)
>>> print(a.storage())
>>> print(a[0].stride)
>>> print(a[0].storage_offset())
# 通常情况下 obj[i][j] = offset+stride[0]*i+stride[1]*j

使用下标得到的subtensor指向原数据,如果不希望更改原本的数据,需要使用.clone()得到数据的拷贝。

二维向量使用.t()可以转置。实际上储存空间没有变,只更改了stride()高维向量使用.transpose(d_1,d_2)可以交换指定的两个维度值。

使用.contiguous()重新排布张量的内部存储使其成为contiguous tensor可以提高运算效率。

使用.dtype查看元素类型。包括int(8、16、32、64),uint8,float(16、32、64),即不同大小的整数和浮点数。默认的Tensor生成的是FloatTensor(float32)。可以在torch.tensor(…, dtype=float)直接指明tensor内部类型。或者使用.float()、.to(float)、.to(dtype=float)转换。

索引方法和python的list差不多。

pytorch的tensor高度兼容numpy。可以使用.numpy()直接转换为numpy的array对象。或者使用torch.from_numpy(array)从numpy的array转化为tensor。

使用torch.save(p,f)和p=torch.load(f)可以存取tensor。通常后缀名为.t

部分pytorch函数支持in-place版本,即a = torch.sqrt(a)可以换成a.sqrt_(),减少空间成本。

Device

使用CUDA的环境中,可以将tensor保存在GPU中。建议在程序开始是检测系统环境。

dev = 'cuda' if torch.cuda.is_available() else 'cpu'

用于GPU的tensor可以使用torch.tensor(…, device=‘cuda’)生成,或者.to(device=‘cuda’)将其转换为CUDA的格式。便于使用CUDA计算。

在多GPU的环境中,使用’cuda:0’等来表示所使用的GPU。

(或者os.enrivon[‘CUDA_VISIBLE_DEVICES’] = ‘0’ ? )

Data

表格数据

可以使用csv.reader或者np.loadtxt处理

csv数据

path = "something.csv"
data_numpy = np.loadtxt(path, dtype=np.float32, delimiter=',', skiprows=1)
# 根据文件情况确定delimiter(, or ;)
# skiprows跳过表头
col_list = next(csv.reader(open(path), delimiter=','))
# 读取到表头
data_tensor = torch.from_numpy(data_numpy)

数值型target可以看作是一个值,也可以转化为独热码。前者存在大小比较的涵义,后者更多用于分类,没有顺序关系。

需要注意原本数据取值范围是基于1(1~max)还是基于0(0~max-1),前者需要减1(scatter是基于0的)

# 独热码生成示例
# target是一个long类型的一维tensor,使用unsqueeze增一个维度
target_oneshot = torch.zeros(target.shape[0],10)
target_oneshot.scatter_(1, target.unsqueeze(1), 1.0)
# 第一个参数表示独热码维数
# 将第三个参数移动到第二个参数表示的位置上

使用zip()可以将多个可迭代数据(list等)打包成元组的list用于迭代。

时间序列数据

在单个数据的基础上增加了时间维度,具有顺序特性。

path = "something.csv"
data_numpy = np.loadtxt(path, dtype=nnp.float32, delimiter=',', skiprows=1,converters={1: lambda x: float(x[8:10])})
# 这里converters表示对第1列用lambda处理
data_tensor = torch.from_numpy(data_numpy)

使用.view()可以改变数据维度,使用-1参数推测维度值

使用cat((tensor_1, tensor_2), dim=1)可以将多个tensor按照目标维度连在一起(目标维度上长度为两者之和,其他维度长度不变)。

数值型数据可以映射到0~1((data-min)/(max-min))或者-1~1或者转变为标准正态分布(data - mean)/std

文本数据

两种方式,针对character的处理和针对word的处理。通常将其转换为独热码。

对于读入的字符串使用.split(’\n’)切成行。或者.replace(’\n’,’ ‘).split()直接得到所有的字符。

使用.lower()可以变为小写,便于分析。使用.strip()删去对应的字符,没有参数时删除首尾空格。

针对character的处理可以按照其ASCII码的数值变为独热码。使用ord()可以得到ASCII数字(0~127)。

针对word的处理可以按照字典文件转变为独热码。或者使用embedding。

# 创建字典示例
with open('path') as f:
  text = f.read()
punctuation = '.,;:"!?”“_-''
# 切分单词
word_list = text.lower().replace('\n', ' ').split()
# 除去标点
word_list = [word.strip(punctuation) for word in word_list]
# 除去重复单词并排序
word_list = sorted(set(word_list))
# 得到字典索引
word2index_dict = {word: i for (i, word) in enumerate(word_list)}

使用embedding可以将字典转化为固定长度的浮点数向量。比较理想的方式是将相近含义或距离比较小的单词映射到距离比较近的向量。通常情况理想的embedding是使用神经网络学习生成的。

(torch.nn.Embedding(num, dim))

图像数据

导入图像的方法很多,包括imageio.imread(),PIL.Image.open(),cv2.imread()等。

import imageio
img_arr = imageio.imread('path')
image_tensor = torch.tensor(img_arr)
# image_tensor = torch.from_numpy(img_arr)

from PIL import Image
image = Image.open('path')
iamge_tensor = torch.tensor(np.array(image))

import cv2
image = cv2.imread('path')
image_tensor = torch.tensor(image)

PyTorch处理的图像要求是C * H * W结构,默认图像为H * W * C因此需要torch.transpose()转换一下。如果是视频,应该得到N * C * H * W。

Tensorflow的图像要求是H * W * C

读取多张图片时可以创建N*C*H*W的tensor再依次读取并存入。或者使用stack()创建。

图像可以根据网络需求进行缩放,旋转和剪切。

通常要进行适当的正规化。

三维数据

例如CT扫描数据,具有三个空间维度。

通常使用5D的tensor表示 N*C*D*H*W。D、H、W表示三个空间维度。C表示通道(通常为一维通道,类似灰度图像)

Model

构建模型

Model Basic

主要成分包括Model(前向传播)、Loss Function、Gradient(反向传播)。

pytorch的tensor提供了requires_grad=True可以自动求导/梯度。指定了自动求导的张量参与的计算会被记录(计算图中的叶子结点),便于求梯度反向传播。拥有.grad成员,默认为None。

指定自动求导的张量参与计算得到的张量(计算图上层节点)拥有.backward()成员,之后原张量(叶子结点)的.grad成员为该张量(上层节点)对应的梯度。

注意这里的.grad梯度值为累计得到,使用完毕需要使用.zero_()归零,防止过分累积。同时新的一次计算时使用.detach()将其从计算图中分离

# 自动梯度举例
def training_loop(n_epochs, learning_rate, params, source, target):
  for epoch in range(1, n_epochs+1):
    if params.grad is not None:
      params.grad.zero_()
    predict = model(source, *params)
    loss = loss_fn(predict, target)
    loss.backward()

    params = (params - learning_rate * params.grad) \
            .detach().requires_grad_()
  return params

同样,显示输出结果等对结果进行操作时必须使用detach()从计算图中分离。

Optim

模型的参数被传入优化器,每当给定输入后计算反向传播和梯度并按照优化器策略自动更新。

优化器包括zero_grad和step成员,前者清空梯度,后者更新参数。

import torch.optim as optim
# 创建优化器
params = torch.tensor([1.0,0.0], requires_grad=True)
learning_rate = 1e-5
optimezer = optim.SGD([params], lr=learning_rate)

# 使用优化器
predict = model(source, *params)
loss = loss_fn(predict, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()

Training、Validation、Overfitting

  • training loss停止下降,可能是网络结构不合适。
  • training loss和validation loss变化趋势相反,可能是过拟合。

使用torch.randperm()可以得到随机排布的参数值用来选择验证集。

n_samples = origin_data.shape[0]
n_val = int(0.2 * n_samples)
shuffled_indices = torch.randperm(n_samples)
train_data = origin_data[shuffled_indices[:-n_val]]
val_data = origin_data[shuffled_indices[-n_val:]]

在计算验证集的损失函数时可以关闭自动梯度功能减轻系统负担

def train_loop(n_epochs, optimizer, params, train_s, train_t, val_s, val_t):
  for epoch in range(1, n_epochs_1):
    train_p = model(train_s, *params)
    train_loss = loss_fn(train_p, train_t)

    with torch.no_grad():
      val_p = model(val_s, *params)
      val_loss = loss_fn(val_p, val_t)
      assert val.loss.requires_grad == False

    optimizer.zero_grad()
    ytain_loss.backward()
    optimizer.step()

建议的做法是使用bool参数控制计算图是否开启反向传播和自动梯度

def calc_forward(source, target, is_train):
  with torch.set_grad_enabled(is_train):
    predict = model(source, *params)
    loss = loss_fn(predict, target)
  return loss

调参思路

  1. learning_rate过大,在极值点附近震荡或者发散。=> 调节学习率。
  2. 梯度的不同分量差距太大,相同的学习率对于梯度的不同分量学习效果不同。=> 尽量保证数量级相同。=> 例如正规化到标准正态分布。
  3. epoch数量不够,尚未达到稳定点。=> 增大epoch次数。
  4. 优化器不合适。=> 换用不同优化器。 => 对应更改学习率。

NN Module

构建神经网络

NN Basic

最简单的Neuron := Linear Transformation + Non-Linear Function(Activation) 简单来说$o = f(w * x + b)$。

网络必须是一个继承自nn.Module的类类型,而且不能是List或者Dict。如果需要的话使用nn.ModuleList或nn.ModuleDict。

数据通常为 N(umber)*C(hannel)*…结构。对应的nn中的模型参数通常为in_channels和out_channels。

单独使用nn中定义的module时使用optim的参数为model的parameters()成员。named_paramster()成员可以得到参数名。

nn中定义了很多Loss Function。例如nn.MSELoss()。

使用nn.Sequential()可以将多层捆绑为一个整体。使用OrderedDict()可以为每一层指定名称,默认使用0-based数字。使用.引导的层次命名访问每一层的数据成员。

框架

使用nn.Module派生的子类,定义.forward()函数给定输入计算得到输出,动态建立计算图即可用于自动求导和优化。

nn包括的没有参数的网络结构,比如这里的Tanh()、ReLU()等可以使用nn.functional实现,减轻网络结构的复杂度。

实际上大部分的网络都可以在functional中找到对应的版本,计算输出时使用函数参数表示网络参数。

# 代码中省略参数
from torch.nn import Module, Conv2d, Dropout2d, Linear
import torch.nn.functional as F
class model_name(Module):
  # 使用pytorch声明网络结构
  def __init__(self):
    # 使用torch.nn中的常见结构构建网络
    # 声明网络的成分
    super().__init__()
    self.conv = Conv2d(...)
    self.drop = Dropout2d(...)
    self.fc1 = Linear(...)
    self.fc2 = Linear(...)

  def forward(self,x):
    # 执行前向传播 指定了网络连接结构
    x = F.relu(F.max_pool2d(self.drop(self.conv(x)), ...))
    x = x.view(...)
    x = F.relu(self.fc1(x))
    x = F.dropout(x,...)
    x = F.log_softmax(self.fc2(x))
    return x

可以直接声明比较复杂的结构,前向传播的时候比较简单

# 例如将卷积激活池化合在一起
self.conv = torch.nn.Sequential(
  torch.nn.Conv2d(...),
  torch.nn.ReLU(...),
  roech.nn.MaxPool2d(...))
# 前向传播可以是
conv_out = self.conv(x)

Dataset

Dataset Basic

使用torch.utils.data的Dataset和DataLoader抽象类可以多线程数据预读取和批量加载。

常见的数据集和训练好的模型大多已经封装在torchvision.datasets之中。

数据正规化可以方便处理,根据数据的特性确定正规化参数。

使用softmax输出的结果选择最大值作为分类输出。

分类可以使用NLLLoss()作为损失函数。

Dataset & DataLoader

Dataset主要的两个成员函数__getitem__()和__len__()。

DataLoader提供对于Dataset的处理,包括batch_size、shuffle、num_workers(子线程数)。

DataLoader得到的是可迭代对象,可以使用next(loader)来获取下一批数据,或者使用for循环(for i, data in enumerate(loader))的方法读取数据用于训练。

通常需要使用transforms来进行预处理,包括ToTensor()、Normalize()、RandomHorizontalFlip()、RandomRotationn()、Resize()等处理方式。

Hand-on

import torch.utils.data as data
import torchvision.transfroms as transforms
class dataset_name(data.Dataset):
  # 创建数据集
  def __init__(self,...):
    self.length = ...
    self.data = [...]
    self.transforms = transforms.Compose([
      transforms.ToTensor(),
      transforms.Normalize(mean=[...], std=[...]),
      transforms.Resize([,,,])
    ])

  def __getitem__(self, item):
    return self.transforms(self.data[item])

  def __len__(self):
    return self.length

# 使用数据集
data_set = dataset_name(...)
data_loader = data.Dataloader(data_set,...)

Visualize

可以使用visdom工具进行pytorch的可视化。

也有使用tensorboardx的可视化方案,功能比较强大, 和tensorflow中的基本相同,使用方法参见pytorch-handbook

Visual Hand-on

使用visdom监控loss或者accuracy的时候

可以使用visdom的追加值绘制曲线

from visdom import Visdom
# 创建一个Visdom的对象
env = Visdom()

# 初始点
x,y=0,0
pane= env.line(
    X=np.array([x]),
    Y=np.array([y]),
    opts=dict(title='dynamic data'))

# 追加新的数据点
for i in range(10):
    time.sleep(1) # 每隔一秒钟打印一次数据
    x+=i
    y=(y+i)*1.5
    print(x,y)
    env.line(
        X=np.array([x]),
        Y=np.array([y]),
        win=pane, # win参数确认使用哪一个pane
        update='append') # 增加一个数据点
Lei Yang
Lei Yang
PhD candidate

My research interests include visual speech recognition and semantics segmentation.