HW1 record

标签: ML by LHY

a

作业介绍

这是这个课程的第一次作业,主要内容: 通过所给的COVID19 的train的五天完整数据以及test的前四天完整数据以及第五天的feature数据,去预测test数据的第五天得病率。 通过题目所给数据集(包含train.csv 和 test.csv),对于train数据集中118列分别包含:

1id 37个state 16个相关特征×5 (5天的数据) 其中每一天的相关数据最后一列为label(得病率)

16个feature:

字段含义
cliCOVID-like illness 比例
iliInfluenza-like illness 比例
hh_cmnty_cli家庭/社区成员有 CLI 症状比例
nohh_cmnty_cli非家庭成员社区 CLI 症状比例
wearing_mask戴口罩比例
travel_outside_state跨州旅行比例
work_outside_home在家外工作比例
shop出门购物比例
restaurant外出就餐比例
spent_time出门花时间比例
large_event参加大型活动比例
public_transit使用公共交通比例
anxious焦虑比例
depressed抑郁比例
worried_finances担忧经济比例
tested_positive检测阳性比例

作业的具体要求:
作业

老师所提供的代码:
代码

1. 代码分析

导入包

import math
import numpy as np
import pandas as pd
import os
import csv
from tqdm import tqdm
import torch
import torch.nn as nn
from torch.utils.data import Dataset,DataLoader,random_split
from torch.utils.tensorboard import SummaryWriter
from sklearn.feature_selection import SelectKBest, f_regression

其中

from tqdm import tqdm

tqdm是python中一个用于美化显示进度条的库

from torch.utils.data import Dataset,DataLoader,random_split

Dataset: 自定义数据集结构
DataLoader:将 Dataset 包装成可批量、可打乱、可多线程加载的可迭代对象,用于训练循环。
random_split: 随机把数据集按比例拆分。

from torch.utils.tensorboard import SummaryWriter

允许你在 PyTorch 中方便地写日志,以便在 TensorBoard 面板实时可视化训练过程。
一般用来记录:
1.loss
2.accuracy
3.learning rate 等

一些优化函数

训练集划分:
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)

预测函数:
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)              
# 在 PyTorch 中,默认会为所有 Tensor 操作建立 计算图(用于自动求梯度),以便在训练时可以进行反向传播(`loss.backward()`)。
# 推理时使用 `torch.no_grad()` 是最佳实践,且必须养成习惯。
        with torch.no_grad():                   
            pred = model(x)                     
            preds.append(pred.detach().cpu())   
    preds = torch.cat(preds, dim=0).numpy()  
    return preds

其中

train_set, valid_set = random_split(...)

返回类型为torch.utils.data.dataset.Subset:包装了原始数据集的子集对象,用于后续 PyTorch DataLoader 中直接取数据使用。

Subset内部结构: Subset: - dataset: 原始完整数据集 - indices: 子集对应的索引(列表)

其中

pre.detach()

返回一个新的张量(tensor),与与张量共享数据,但可以从原计算图中分离,不再被autograd跟踪

如果你想:
1.将 pred 保存下来用于日志记录
2.移动到 CPU
3.转为 NumPy
4.做可视化分析
5.或用于后续非训练逻辑中使用

就应当使用

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)

Neural Network 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

后续优化有对network的优化

Feature Selection

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

Training loop

def trainer(train_loader, valid_loader, model, config, device):

    criterion = nn.MSELoss(reduction='mean') # Define your loss function, do not modify this.

    # Define your optimization algorithm. 
    # TODO: Please check https://pytorch.org/docs/stable/optim.html to get more available algorithms.
    # TODO: L2 regularization (optimizer(weight decay...) or implement by your self).
    optimizer = torch.optim.SGD(model.parameters(), lr=config['learning_rate'], momentum=0.9) 
    
    writer = SummaryWriter() # Writer of tensoboard.

    if not os.path.isdir('./models'):
        os.mkdir('./models') # Create directory of saving models.

    n_epochs, best_loss, step, early_stop_count = config['n_epochs'], math.inf, 0, 0

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

        # 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)

        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
            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

其中

 x, y = x.to(device), y.to(device)
  • x.to(device) 会返回一个新的 tensor(在 device 上)

  • 原来的 x 不会被原地修改

loss.backward()
`loss.backward()` 并不会更新权重参数本身,只是把梯度算好。
optimizer.step()
  • 优化器会 遍历模型中所有可训练参数

  • 使用 .grad 中的值

一般步骤为:

“清零 → 正向 → 计算损失 → 反向(算梯度) → 优化(更新参数)”:

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()

⭐一些细碎的知识:

 loss_record.append(loss.detach().item())
loss.detach()

功能: 创建一个与原张量内容相同的新张量,但这个新张量不再是计算图的一部分。这意味着对新张量的任何操作都不会被记录下来以供反向传播。它会中断从这个点开始的梯度流。
可用于特殊的阶段梯度流:
假设你有一个复杂的模型,其中包含两个子模型 model_Amodel_B。你希望 model_B 的训练依赖于 model_A 的输出,但是你不希望 model_B 的梯度反向传播回去影响 model_A 的参数。

import torch
import torch.nn as nn

class ModelA(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(10, 5)

    def forward(self, x):
        return self.linear(x)

class ModelB(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(5, 1)

    def forward(self, x):
        return self.linear(x)

model_A = ModelA()
model_B = ModelB()

# 示例输入
input_data = torch.randn(1, 10) # 1个样本,10个特征

# 正常情况:梯度会流经两个模型
output_A = model_A(input_data)
output_B = model_B(output_A)
loss_full_graph = output_B.sum()
loss_full_graph.backward() # 梯度会计算并更新 model_A 和 model_B 的参数


# 清除梯度
model_A.zero_grad()
model_B.zero_grad()

# 使用 detach() 截断梯度流
output_A_detached = model_A(input_data).detach() # 关键在这里!
output_B_detached_path = model_B(output_A_detached)
loss_detached_path = output_B_detached_path.sum()
loss_detached_path.backward() # 只有 model_B 的梯度会被计算和更新

相当于将model_A那一部分从计算图中拿出
loss_detached_path.backward() 被调用时,梯度只能反向传播到 model_B。因为 output_A_detached 不在计算图中,梯度无法从 output_A_detached 向前传到 model_A。因此,model_A 的参数将不会被更新。

	loss.item()

功能: 直接将一个包含单个元素的 PyTorch 张量转换为标准的 Python 标量(floatint)。

DataLoader

从文件中读取数据并设置训练集、验证集和测试集。

# Set seed for reproducibility
same_seed(config['seed'])


# train_data size: 2699 x 118 (id + 37 states + 16 features x 5 days) 
# test_data size: 1078 x 117 (without last day's positive rate)
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'])

# 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)

start train

model = My_Model(input_dim=x_train.shape[1]).to(device) # put your model and data on the same computation device.
trainer(train_loader, valid_loader, model, config, device)

Testing

def save_pred(preds, file):
    ''' Save predictions to specified file '''
    with open(file, 'w') as fp:
        writer = csv.writer(fp)
        writer.writerow(['id', 'tested_positive'])
        for i, p in enumerate(preds):
            writer.writerow([i, p])

model = My_Model(input_dim=x_train.shape[1]).to(device)
model.load_state_dict(torch.load(config['save_path']))
preds = predict(test_loader, model, device) 
save_pred(preds, 'pred.csv')  

2.相关优化

最开始的时候,使用老师提供的代码得到的结果是:

Private Score: 1.61450 Public Score:1.56370

显然这个效果是最普通的效果,所以需要对其进行优化。

(1)修改参数

例如:lr 、batch等

一般对于调参,基本都是×10 or %10

(2) 修改网络层

一般来说,网络层优化并没有所谓越深越好的道理,甚至有所谓的“大道至简”的说法。 ⭐:奥卡姆剃刀原则 : 降低模型复杂度,可以提高泛化性

i 过拟合

在这个题目中,当发现训练loss与test的结果出现比较大的偏差,即两者结果逐渐拉大的时候,即出现过拟合

则可以将模型直接减到一层,只有一个Linear层,之后逐渐将宽度,深度往上加

ii 欠拟合

若不是过拟合,则应该是模型性能欠佳,则是欠拟合

一般是模型复杂度不够,可以将模型加宽,加深,或者使用一些激活函数,例如:ReLU等。

也可以提高lr

(3) 优化器

i 修改优化器

原本代码中使用的是SGD(随机梯度下降)一阶优化器,我们可以修改为AdamW二阶优化器(二阶优化器带有动量(momentum))

同时AdamW中自带Weight_Decay(L2正则化),也可以通过参数调节进行优化

ii 使用闭包

        for x, y in train_pbar:
            x, y = x.to(device), y.to(device)
            def closure():
                optimizer.zero_grad()               # Set gradient to zero.   # Move your data to device. 
                pred = model(x)             
                loss = criterion(pred, y)
                loss.backward()         
                return loss          
              # Compute gradient(backpropagation).
            loss = closure()                        # Compute loss.
            optimizer.step(closure)                    # 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()})

可以见到,代码中将设置梯度为0,移动数据到device,计算loss,计算梯度,更新参数,都放在了闭包中

closure 本质是:

一个封装“前向传播 + 计算损失 + 反向传播”的函数,供优化器在内部多次调用时使用。

闭包一般用于:
用于需要多次评估模型和梯度的优化器,如:

1.L-BFGS

2.部分二阶优化器

在这些优化器中,可能需要:

多次前向传播计算损失

多次反向传播计算梯度

通过 closure,优化器内部即可多次自动调用并获取最新梯度和损失。

同时需要注意代码中

            loss = closure()                        # Compute loss.
            optimizer.step(closure) 

这两行代码的顺序,如果将这两行代码的顺序颠倒,那么loss的值将会是上一次的loss值,而不是当前loss值。

(4)挑选合适的feature进行训练

代码中Feature Selection部分其实十分重要,经过简单思考我们不难发现,貌似得病率与state基本没有关系(直觉上),于是将其剔除

这个题目其实有多种feature的选择:

i 只通过前四天的得病率进行训练

可以将其理解成老师在课上所讲的对于Youtube流量预测的例子,则无需关注其他的特征

ii 将state数据剔除后的数据进行训练

即上文所提到的,得病率与state基本没有关系

iii 通过类似特征工程的方法

代码如下:

首先导入相关的包

from sklearn.feature_selection import SelectKBest, f_regression

之后通过内置函数进行特征选择,选取出15个与得病率最相关的特征进行训练

# --- 核心修改:在训练集上进行特征选择并获取索引 ---
k = 15 # 选择 K 个最佳特征的数量

# 分离训练集的特征和目标,用于特征选择
train_features_for_selection = train_data_full[:, :-1]
train_target_for_selection = train_data_full[:, -1]

# 初始化并 fit SelectKBest
feature_selector = SelectKBest(score_func=f_regression, k=k)
feature_selector.fit(train_features_for_selection, train_target_for_selection)

# 获取从训练集学习到的最佳特征索引
idx = np.argsort(feature_selector.scores_)[::-1]
best_feat_indices = idx[:k]

# --- 应用特征选择到所有数据集 ---

# 应用到训练集
train_data, train_target = select_feat(train_data_full, None, best_feat_indices, is_train_or_valid=True)

# 应用到验证集
valid_data, valid_target = select_feat(valid_data_full, None, best_feat_indices, is_train_or_valid=True)

# 应用到测试集 (注意 test_data 的结构:它可能只有特征,没有目标)
# 假设 test_data 已经加载且只包含特征列
test_data = select_feat(test_data, None, best_feat_indices, is_train_or_valid=False)

最后结果也是成果得到了优化,最优结果如下:

Private Score: 0.92087 Public Score:0.88337