Pytorch tutorial and Regression


[ML] Pytorch tutorial and Regression

时隔两年重新开始写博客,毕设基本结束,现在先开始跟着李宏毅的课程,系统学习一下。

Pytorch Tutorial

数据读入

dataset和dataloder,dataset读入(stores data samles),dataloder使用(groups data in batches)

from torch.utils.data import Dataset,DataLoader

dataset = MyDataset(file)
dataloder = DataLoader(dataset,batch_size,shuffle=True)
#train的时候开shuffle,test的时候关

dataloader每次按序(shuffle=false的情况)从dataset中取出batch_size个变成一个batch。

关于为什么要shuffle的说明数据集shuffle的重要性 - 知乎 (zhihu.com)

定义dataset

# 定义MyDatasetfrom torch.utils.data import Dataset,Dataloader

class MyDataset(Dataset):
    #初始化,对data处理
    def __init__(self,file):
        self.data = =....
    #返回单个
    def __getitem__(self,index):
        return self.data[index]
    #获取dataset长度
    def __len__(self):
        return len(self.data)

Tensors

Tensor是高维度的矩阵,是信息读入后存在Pytorch中的形式。

dim in Pytorch == axis in NumPy\

一些常用操作

# 用shape看大小
x.shape()

# list to tensor
x = torch.tensor([1,0],[0,1])

# numpy to tensor
x = torch.from_numpy(np.array([1,0],[0,1]))

# zeros和ones
x = torch.zeros([2,2,2])# generate a 2*2*2 3dim tensor filled with 0
# rand
x = torch.rand([2,2,2]) # generate a 2*2*2 random 3dim tensor

# 常见运算+-,sum(),mean(),pow()
y = x.pow(2)

# transpose,指定两个dimension互换,如2*3变3*2
y = x.transpose(0,1) # change dim0 and dim1


# squeeze,拿掉长度为1的dimention,如
x = torch.zeros([1,2,3]) #generate a 1*2*3 tensor
x = x.squeeze(0) #squeeze to an 2*3 tensor

x = x.unsqueeze(1) # unsqueeze to an 2*1*3 tensor

# cat,把多个tensor连接在一起
x = torch.zeros([2,1,3])
y = torch.zeros([2,3,3])
z = torch.zeros([2,2,3])


w = torch.cat([x,y,z],dim = 1)
# 沿dim1方向连接
# torch size [2,6,3]

Tensor - Datatype

# 32bit float
torch.float
torch.FloatTensor

# 64bit integer
torch.long
torch.LongTensor

#use dtype to check the datatype
x.dtype

Tensor - Device

Tensor will be computed with CPU by default.

use to() to move tensor to appropriate device like:

x = x.to('cpu')

# to gpu
x = x.to('cuda')
# multi gpu
x = x.to('cuda:0') #cuda:1,cuda:2.....

Tensor - Gradient

使用pytorch提供的函数求梯度

z.backward求z微分,用x.grad看结果

gradient.png

训练前

进入训练之前三件事情,定好layers,损失函数loss function,optimization algorithm最佳化演算法

Linear Layer(fully connected layer)

linear layer包含一组bias和一组weight

gradient.png

如图即让(?,32)大小的输入x乘上(32,64)的W,再加上b。先做一次矩阵乘法再做一次加法。$y = W\cdot x+b$

layer = torch.nn.Linear(32,64)
layer.weight.shape
layer.bias.shape
# 可以用weight和bias.shape看linear层中w和b参数的大小

# 参数
nn.Linear(in_feature,out_feature,bias)

两种定义网络结构的方法

关于super(Mymodel,self).__init__()这句简言之就是对继承自父类nn.Module的属性进行初始化。super()是用来用于调用父类的一个方法。这里参考了(解惑(一) —– super(XXX, self).init()到底是代表什么含义_self.fc3是_底层研究生的博客-CSDN博客

import torch.nn as nn

# 再自己定义网络时,需要继承nn.Module类,并且重新实现构造函数Init和forwawrd两个方法

class MyModel(nn.Module):
  # 第一种方法,使用序列容器Sequential
	def __init__(self):
    super(Mymodel,self).__init__()
    self.net = nn.Sequential( # sequential是pytorch的一个序列容器
    	nn.Linear(10,32),
      nn.Sigmoid(),
      nn.Linear(32,1)
    )
  def forward(self,x): # 前向传播
    return self.net(x)

class MyModel(nn.Module):
  # 第二种方法,再forward中相继调用
	def __init__(self):
    super(Mymodel,self).__init__()
    self.layer1 = nn.Linear(10,32)
    self.layer2 = nn.Sigmoid()
    self.layer3 = nn.Linear(32,1)
  def forward(self,x): # 前向传播
    out = self.layer1(x)
    out = self.layer2(out)
    out = self.layer3(out)
    return out

torch.optim

最佳化演算法即选择调整model参数的方法,比如SGD演算法,在torch.optim这个library中。

torch.optim.SGD(model.parameters(),lr,momentum = 0)

再定好最佳化演算法后,在每个batch都需要有这三步。

optimizer.zero_grad() # 把上一步的gradient归零
loss.backward() # 计算loss的gradient
optimizer.step() # 利用gradient调整model的参数

完整训练过程

关于Training,validation,testing的区别训练集(train)验证集(validation)测试集(test)与交叉验证法 - 知乎 (zhihu.com)

Training Loop训练

dataset = Mydataset(file) #read data
tr_set = DataLoader(dataset,16,shuffle=True) # put dataset into dataloader
model = MyModel().to(device) # device 可以是 cpu/cuda
criterion = nn.MSELoss() #选择损失函数
optimizer = torch.optim.SGD(model.parameters(),lr,momentum = 0) #选择optimizer

for epoch in range(n_epoch):
  model.train() # 这句是把model设置成training mode
  for x,y in tr_set:
    optimizer.zerograd() # 1.把上一步的gradient归零
    x,y = x.to(device) , y.to(device) #放到设备上去
    pred = model(x)
    loss = criterion(pred,y) # 预测值和期望结果计算loss
    loss.backward() # 2.计算loss的gradient
    optimizer.step() # 3.利用gradient调整model的参数

Validation Loop验证

用于在训练过程中检验模型的状态,收敛情况

model.eval() # 这句是把model设置成evalidation mode
total_loss = 0

for x,y in dv_set:
  x,y = x.to(device),y.to(device)
  with torch.no_grad(): #关闭梯度
 		pred = model(x)
    loss = criterion(pred,y)
  total_loss += loss.cpu().item() * len(x) # 收集loss
  avg_loss = total_loss / len(dv_set.dataset) # 计算平均loss

Testing Loop测试

model.eval()
preds = []
for x in tt_set:
  x = x.to(device)
  with torch.no_grad(): #和evalidation一样关闭梯度
    pred = model(x)
    preds.append(pred.cpu()) # 把预测值pred手机到preds中去

model的一些layers在testing和training的时候的行为时不一样的,比如dropout,所以需要开evalidation mode。

在testing和validation的时候我们比希望model学习到这些资料,所以这时候用torch.no_grad把梯度关掉。

save/load models

# save model
torch.save(model.state_dict() , path)

# load model
ckpt = torch.load(path)
model.load_state_dict(ckpt)

官方文档

PyTorch documentation — PyTorch 2.0 documentation

常用套件

torch.nn -> neural network

torch.optim -> optimize function

Torch.utils.data -> dataset,dataloader

一些常见error

Tensor on Different Device to model

model和input要放到一个设备上

# 正确写法
model = torch.nn.Linear(5,1).to("cuda:0")
x = torch.rand(5).to("cuda:0")

Mismatched Dimensions

不同大小的矩阵相加,用transpose使大小相同

x = torch.randn(4,5)
y = torch.randn(5,4)

# transpose
y = y.transpose(0,1) # transpose dim1 and dim0
z = x + y

Cuda Out of Memory

batch size of data is too larch to fit in GPU,应当减少batch size。

如果batch已经减少到1了,则可能是model太大,考虑减少model大小。

Mismatched Tensor Type

需要转换tensor的类型,如换成long,用label = label.long()

Regression

老师课上的举例,回归问题就是找function,如最简单的可能就是$y = w\cdot x+b$,其中w为weight,b为bias。

reg.png

Liear model线性模型$y=b+\sum w_ix_i$

Loss function

Input:a function

Output:how bad it is

衡量一组参数的好坏$L(f) = L(w,b)$

使用估计值-实际值取平方相加并求和,即$L(w,b) = \sum_{n=1}^{10}(\hat{y}^n-(w\cdot x^n+b))^2$

找loss最小的function,写作$f^* = arg \min_fL(f)$

$w^*,b^* = arg\min_{w,b}L(w,b)$

使用gradient descent梯度下降来找这个f

Gradient Descent

移动参数 $-\eta \frac{dL}{dw}|_{w=w_0}$,其中$\eta$是learning rate

如果有两个参数w,b的情况,一样是计算$\frac{\partial L}{\partial w}|{w=w^0}$和$\frac{\partial L}{\partial b}|{b = b^0}$两个偏微分

在二维上理解,梯度方向就是等高线的方向。

reg.png

在Linear regression问题中不存在local optimal,所以不需要考虑找到的只是局部最优。

reg.png

In Linear Regression the loss function L is convex,故不用担心沿着gradient方向找到的只是局部最优解的情况了。即在Linear Regression问题中,Loss function关于w和b的图如上面的图,是形如一个椭圆形的。

reg.png

用求偏导的方式计算Lossfunction对w和b的偏微分,过程如上。

有时候这种wx+b的模型太简单,可以尝试引入二次项,如$y=b+w_1\cdot x+w_2\cdot x^2$

效果比单纯一次的模型要更好,同理,可以再增加三次项、四次项$y=b+w_1\cdot x+w_2\cdot x^2 + w_3\cdot x^3$

over fitting

老师在把模型增加到五次的时候,在trainning data的表现有继续变优,但是testing data反而效果变差了

overfit

A more complex model does not always lead to better performance on testing data!This is called OverFitting.

所以选择合适的model是一件重要的事情,比如图中的情况显然应当选择3次的model。

of.png

regularization

在现有Features不变情况下,降低部分不重要Features的影响力。这个方法有助于有很多Features且每个Features都有贡献的Neural Network避免Overfitting。

regu.png

老师的话是,regularization将w平方乘以$\lambda$加入损失函数,有利于让function更加平滑,在实际实验中,regularization能有效的避免overfitting,在训练集中的loss可能会变差,但是在testing中的效果会提升。

在trainning和testing的影响

Colab tutorial

Google Colab Tutorial 2023 - Colaboratory

Google Colab 快速上手 - 知乎 (zhihu.com)

很重要!colab防断线

(160条消息) colab防止掉线断连及清除方法_colab断开连接_jinniulema的博客-CSDN博客

function ConnectButton(){
    console.log("Connect pushed"); 
    document.querySelector("#top-toolbar > colab-connect-button").shadowRoot.querySelector("#connect").click() 
}
setInterval(ConnectButton,60000);

# 结束
clearInterval(intervalId) # intervalId换成具体的数字

作业1 : 新冠预测Regression

给定1,2,3天的资料,每天的资料是survey和positive cases

input

  • 输入包括一个35维度的one-hot vector表示州,后面还跟着一系列feature,其中第三天的test positive case是我们需要预测的

feature

feature2

  • one-hot vector就是一个向量里面只有一个元素为1,其余全是0。在作业中,所在州的编码就是一个onehot vector。

onehot vector使用MSE(mean squared error)均方误差

关于feature的大小,打印如下,88个特征我认为是35个州,加上三天的特征(5+5+2+2+3+1)*3 = 54 ,因为最后一天的那个1,感染人数是要预测的,所以-1,即35+54-1 = 88个特征值,一个预测值。

88个feature

sample code复现

固定seed

def same_seed(seed): 
    '''Fixes random number generator seeds for reproducibility.'''
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False
    np.random.seed(seed)
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed_all(seed)

为了固定训练用的seed,让训练结果能够复现,一共四种随机种子,都输入预设好的seed值,在config中可以设置

train 和 valid data的划分

def train_valid_split(data_set, valid_ratio, seed):
    '''Split provided training data into training set and validation set'''
    valid_set_size = int(valid_ratio * len(data_set)) 
    train_set_size = len(data_set) - valid_set_size
    train_set, valid_set = random_split(data_set, [train_set_size, valid_set_size], generator=torch.Generator().manual_seed(seed))
    return np.array(train_set), np.array(valid_set)

valid_ratio在config中可以设置的一个比例。

random_split把seed输进去

predict预测

def predict(test_loader, model, device):
    model.eval() # Set your model to evaluation mode.
    preds = []
    for x in tqdm(test_loader):
        x = x.to(device)                        
        with torch.no_grad():                   
            pred = model(x)                     
            preds.append(pred.detach().cpu())   
    preds = torch.cat(preds, dim=0).numpy()  
    return preds

比较直观,set成evalidation mode,tqdm是一个用于展示迭代进度条的库

在preds用append()函数往后面插入结果。

pred是当前这次预测的结果,这里的pred.detach().cpu()我查阅资料后的理解:

pred是device:cuda得到的cuda tensor,在官方文档对detach()的说明是返回一个不包含gradient梯度的tensor(detach的含义是分离)

Returns a new Tensor, detached from the current graph.
The result will never require gradient.

但是pred.detach()是一个gpu类型的tensor,这时候调用cpu(),转化成cpu tensor类型才可以被list读取。

dataset定义

class COVID19Dataset(Dataset):
    '''
    x: Features.
    y: Targets, if none, do prediction.
    '''
    def __init__(self, x, y=None):
        if y is None:
            self.y = y
        else:
            self.y = torch.FloatTensor(y)
        self.x = torch.FloatTensor(x)

    def __getitem__(self, idx):
        if self.y is None:
            return self.x[idx]
        else:
            return self.x[idx], self.y[idx]

    def __len__(self):
        return len(self.x)

包括初始化init,取单个元素getitem,取长度len

定义model

class My_Model(nn.Module):
    def __init__(self, input_dim):
        super(My_Model, self).__init__()
        # TODO: modify model's structure, be aware of dimensions. 
        self.layers = nn.Sequential(
            nn.Linear(input_dim, 16),
            nn.ReLU(),
            nn.Linear(16, 8),
            nn.ReLU(),
            nn.Linear(8, 1)
        )

    def forward(self, x):
        x = self.layers(x)
        x = x.squeeze(1) # (B, 1) -> (B)
        return x

这里的super之前讲过是调用父类nn.Module的init函数

然后也有了我们第一个任务,即完成对layers的修改

这里的layers输入第一层就是input_dim,后来想了一下,其实这么设定的原因是因为有select_feat选择特征函数,所以这里的输入vector维度是不确定的。

特征选择

通过select_feat函数减少特征的维度,即设定传入哪些特征

def select_feat(train_data, valid_data, test_data, select_all=True):
    '''Selects useful features to perform regression'''
    y_train, y_valid = train_data[:,-1], valid_data[:,-1]
    raw_x_train, raw_x_valid, raw_x_test = train_data[:,:-1], valid_data[:,:-1], test_data

    if select_all:
        feat_idx = list(range(raw_x_train.shape[1]))
    else:
        feat_idx = [0,1,2,3,4] # TODO: Select suitable feature columns.
        
    return raw_x_train[:,feat_idx], raw_x_valid[:,feat_idx], raw_x_test[:,feat_idx], y_train, y_valid

这里其实诸如train[:,-1]这样的语法不太理解。

train[:, -1], 是说对train这个二维的数据,逗号分隔开的前面的”:”是说取全部的行,逗号后面的-1是说取最后一列。

如果换成一维数组会容易理解,比如list[:]取全部的列 以及list[-1]取最后一个。

而后面的b = a[i:j]

表示复制a[i]到a[j-1],以生成新的list对象

a = [0,1,2,3,4,5,6,7,8,9]
b = a[1:3]  # 得到[1,2]

而如果不填,则默认为0,所以代码中的[:,:-1]其实是[:,0:-1]

trainning

torch.optism.SGD参数:第一个是网络的参数,第二个lr是learning rate,第三个momentum是冲量,即$v’ = -dx*lr + v\cdot momentum$即每次的更新量在之前的梯度计算基础上,还要加上上次的更新量乘上一个momentum。

for epoch in range(n_epochs):
      model.train() # Set your model to train mode.
      loss_record = [] #记录loss

      # tqdm is a package to visualize your training progress.
      train_pbar = tqdm(train_loader, position=0, leave=True)

      for x, y in train_pbar:
          optimizer.zero_grad()               # Set gradient to zero.
          x, y = x.to(device), y.to(device)   # Move your data to device. 
          pred = model(x)             
          loss = criterion(pred, y)
          loss.backward()                     # Compute gradient(backpropagation).
          optimizer.step()                    # Update parameters.
          step += 1
          loss_record.append(loss.detach().item())
          
          # Display current epoch number and loss on tqdm progress bar.
          train_pbar.set_description(f'Epoch [{epoch+1}/{n_epochs}]')
          train_pbar.set_postfix({'loss': loss.detach().item()})

      mean_train_loss = sum(loss_record)/len(loss_record)
      writer.add_scalar('Loss/train', mean_train_loss, step)

每个epoch中分train和validation两个部分

model.eval() # Set your model to evaluation mode.
    loss_record = []
    for x, y in valid_loader:
        x, y = x.to(device), y.to(device)
        with torch.no_grad():
            pred = model(x)
            loss = criterion(pred, y)

        loss_record.append(loss.item())
        
    mean_valid_loss = sum(loss_record)/len(loss_record)
    print(f'Epoch [{epoch+1}/{n_epochs}]: Train loss: {mean_train_loss:.4f}, Valid loss: {mean_valid_loss:.4f}')
    # writer.add_scalar('Loss/valid', mean_valid_loss, step)

    if mean_valid_loss < best_loss:
        best_loss = mean_valid_loss
        torch.save(model.state_dict(), config['save_path']) # Save your best model
        # 即如果当前的loss比bestloss更好,才会save model
        print('Saving model with loss {:.3f}...'.format(best_loss))
        early_stop_count = 0
    else: 
        early_stop_count += 1

    if early_stop_count >= config['early_stop']:
        print('\nModel is not improving, so we halt the training session.')
        return

在validation中将mode设置为eval,另外和之前pytorch tutorial中一样,关闭grad,主要目的就是因为validation是不改变参数的。

这里还添加了一个earlystop,即通过config中设置的参数early_stop(默认600),当模型经过600次都没有改进,则停止训练。

dataloader

same_seed(config['seed'])
train_data, test_data = pd.read_csv('./covid_train.csv').values, pd.read_csv('./covid_test.csv').values
train_data, valid_data = train_valid_split(train_data, config['valid_ratio'], config['seed'])

# Print out the data size.
print(f"""train_data size: {train_data.shape} 
valid_data size: {valid_data.shape} 
test_data size: {test_data.shape}""")

# Select features
x_train, x_valid, x_test, y_train, y_valid = select_feat(train_data, valid_data, test_data, config['select_all'])
#通过select_feat中的

# Print out the number of features.
print(f'number of features: {x_train.shape[1]}')

train_dataset, valid_dataset, test_dataset = COVID19Dataset(x_train, y_train), \
                                            COVID19Dataset(x_valid, y_valid), \
                                            COVID19Dataset(x_test)

# Pytorch data loader loads pytorch dataset into batches.
train_loader = DataLoader(train_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True)
valid_loader = DataLoader(valid_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True)
test_loader = DataLoader(test_dataset, batch_size=config['batch_size'], shuffle=False, pin_memory=True)

输出的训练集,valid集和test集的大小如下,由于没做featureselect ,所以还是88维的输入。

train_data size: (2408, 89) 
valid_data size: (601, 89) 
test_data size: (997, 88)
number of features: 88

todolist

  • modify model’s structure, be aware of dimensions. 更改layers
  • Select suitable feature columns.选择重要的feature
  • Define your optimization algorithm.选择optimization

optimization

我做了什么

  • 在colab上保存了sample code的副本,阅读代码

  • 通过google drive导入数据集

    colab

  • 运行sample code生成pred.csv,上传到kaggle上,得分(1.68198,185034)

  • 尝试修改layers层,增加了一层

nn.Linear(input_dim, 16),
      nn.ReLU(),
      nn.Linear(16, 8),
      nn.ReLU(),
      nn.Linear(8, 4)
      nn.ReLU(),
      nn.Linear(4, 1)
      # add a layer
      # 原本是nn.Linear(8, 1)就结束了的

最后上传到kaggle的效果反而变差了(3.90553,3.28428)

  • 尝试增加instancenorm1d归一化层
nn.Linear(input_dim, 32),
      nn.InstanceNorm1d(32),
      nn.ReLU(),
      nn.Linear(32, 8),
      nn.InstanceNorm1d(8),
      nn.ReLU(),
      nn.Linear(8, 1),
      # 最后这里加不加激活函数呢?
      # nn.InstanceNorm1d(1),
      # nn.ReLU(),

使用归一化后效果还不如最开始什么都不加的。。

思考了一下会不会是因为之前增加层数导致需要更多的epoch,所以尝试一下增加层数并且增加归一化层。

代码复现

torch.optism.SGD参数:第一个是网络的参数,第二个lr是learning rate,第三个momentum是冲量,即$v’ = -dx*lr + v\cdot momentum$即每次的更新量在之前的梯度计算基础上,还要加上上次的更新量乘上一个momentum。


文章作者: REXWind
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 REXWind !
评论
  目录