说在前面
作为李宏毅机器学习2021的第一个作业,也是我接触的第一个深度学习项目,从看懂到sample code到能够逐步接近各项baseline真的很不容易,下面是我将自己完成项目的总结和借鉴的大神的工作汇总整理如下,希望能对像当时的我一样对机器学习一头雾水的朋友入门。待我整理好后我会将我的完整代码公布在此处。
Data Analysis
本作业提供了两个文件
-
covid.train.csv 训练集
-
covid.test.csv 测试集
打开 covid.train.csv
可以看到训练集数据,一共有95列。第1列为ID号,2-41列为 课件中所说的 one-hot-vector 的数据表示该病例是属于哪个state,后面的数据则为跟新冠有关的信息,仔细观察可以发现是三天的新冠诊断数据。
打开 covid.test.csv
可以看到测试集数据,一共94列。格式与 covid.train.csv
一致,缺少95列 第三天的 tested_postive 的数据,这一列便是题目要求我们预测的数据。
另外因为没有提供验证集, 我们需要自行从训练集中划分验证集, 并使用划分后的训练集和验证集来完成交叉验证,提高准确性,从而达到更低的private score.
Simple Baseline
simple baseline score : 2.03004
成功运行课程提供的sample code,提交其预测结果即可达到simple baseline.
Medium Baseline
medium baseline score: 1.28359
课程slide的提示
Feature selection: 40 states + 2
tested_positive
(TODO
in dataset)
根据提示我们在sample code中的Dataset 函数中增加Feature selection的功能,筛选的特征为40 states + 2 tested_positive
,即40个洲和2天的检验阳性。
提醒:
-
启用 feature selection 功能,需将 target_only变量设为 True,请仔细检查训练时该变量的值是否为 True.
-
在读取csv文件时,已自动去除第一行(列名)和第一列(id),并转换为numpy数组,意味着剩下的列号是从0开始计数的.
class COVID19Dataset(Dataset):
''' Dataset for loading and preprocessing the COVID19 dataset '''
def __init__(self,
path,
mode='train',
target_only=True):
self.mode = mode
# Read data into numpy arrays
with open(path, 'r') as fp:
data = list(csv.reader(fp))
data = np.array(data[1:])[:, 1:].astype(float) # remove the first row and first line
if not target_only:
feats = list(range(93))
else:
# TODO: Using 40 states & 2 tested_positive features (indices = 57 & 75)
feats = list(range(40)) + [57, 75]
pass
if mode == 'test':
# Testing data
# data: 893 x 93 (40 states + day 1 (18) + day 2 (18) + day 3 (17))
data = data[:, feats]
self.data = torch.FloatTensor(data)
else:
# Training data (train/dev sets)
# data: 2700 x 94 (40 states + day 1 (18) + day 2 (18) + day 3 (18))
target = data[:, -1]
data = data[:, feats]
# Splitting training data into train & dev sets
if mode == 'train':
indices = [i for i in range(len(data)) if i % 10 != 0]
elif mode == 'dev':
indices = [i for i in range(len(data)) if i % 10 == 0]
# Convert data into PyTorch tensors
self.data = torch.FloatTensor(data[indices])
self.target = torch.FloatTensor(target[indices])
# Normalize features (you may remove this part to see what will happen)
self.data[:, 40:] = \
(self.data[:, 40:] - self.data[:, 40:].mean(dim=0, keepdim=True)) \
/ self.data[:, 40:].std(dim=0, keepdim=True)
self.dim = self.data.shape[1]
print('Finished reading the {} set of COVID19 Dataset ({} samples found, each dim = {})'
.format(mode, len(self.data), self.dim))
修改后进行训练,将预测结果提交平台结果如下,达到medium baseline.
Strong Baseline
strong baseline score: 0.88017
课程slide的提示
Feature selection (what other features are useful?)
DNN architecture (layers? dimension? activation function?)
Training (mini-batch? optimizer? learning rate?)
L2 regularization
There are some mistakes in the sample code, can you find them?
我就按照课程的提示依次来改进模型
1. Feature Selection 特征选择
参考资料:特征选择方法最全总结!-CSDN博客
特征选择的部分作用:
● 减少训练数据大小,加快模型训练速度。
● 减少模型复杂度,避免过拟合。
● 特征数少,有利于解释模型。
● 如果选择对的特征子集,模型准确率可能会提升
● 去除冗余无用特征,减低模型学习难度,减少数据噪声。
● 去除标注性强的特征,例如某些特征在训练集和测试集分布严重不一致,去除他们有利于避免过拟合。
我们使用Feature Selection顺利地通过了 Medium Baseline 。我们应该选择与预测结果高度相关的特征用于训练,但是我们并不知道在过 medium baseline 时我们选取的特征是否是最相关的,所以我们应该完善我们的 Feature Selection.
特征选择思路:
-
使用sklearn的feature_selection工具中的f_regression方法,计算得出与目标显著相关的特征。
-
选取相关性高的特征(由高到低选取,个数需要实践根据实践效果判断)
在sample code 中增加 Feature Selection 模块
import pandas as pd
from sklearn.feature_selection import SelectKBest, f_regression
# 读取数据
data = pd.read_csv(r'./covid.train.csv') # 读取CSV文件
x = data[data.columns[1:94]] # 选择特征列(第2列到第94列),第1列是非特征列 id
y = data[data.columns[94]] # 选择目标列(第95列)
# 特征归一化
x = (x - x.min()) / (x.max() - x.min()) # 将特征值缩放到0到1之间
# 创建SelectKBest实例
bestfeatures = SelectKBest(score_func=f_regression) # 选择K个最佳特征,这里使用f_regression作为评分函数
# 计算所有特征的分数
fit = bestfeatures.fit(x, y) # 计算每个特征的分数
# 将分数转换为DataFrame
dfscores = pd.DataFrame(fit.scores_) # 将特征分数转换为DataFrame格式
# 创建包含特征名称的DataFrame
dfcolumns = pd.DataFrame(x.columns) # 将特征列名转换为DataFrame格式
# 将两个DataFrame进行合并以便于可视化
featureScores = pd.concat([dfcolumns, dfscores], axis=1) # 合并特征名称和分数DataFrame
# 为合并后的DataFrame命名列
featureScores.columns = ['Specs', 'Score'] # 将列名设置为'Specs'和'Score'
# 打印前20个分数最高的特征
print(featureScores.nlargest(20, 'Score')) # 显示得分最高的20个特征
# 打印得分最高特征的索引
top_rows = featureScores.nlargest(20, 'Score').index.tolist()[:17] # 获取得分最高的前17个特征的索引
print(top_rows) # 打印这些特征的索引
运行结果如下:
根据评分结果,我们会选取前14位作为我们使用的特征,因为从15位开始得分差距较大。但是根据我们的实践结果显示选择前17位效果更好
修改Dataset模块代码,选取前17位特征进行训练。
class COVID19Dataset(Dataset):
''' Dataset for loading and preprocessing the COVID19 dataset '''
def __init__(self,
path,
mode='train',
target_only=True):
self.mode = mode
# Read data into numpy arrays
with open(path, 'r') as fp:
data = list(csv.reader(fp))
data = np.array(data[1:])[:, 1:].astype(float) # remove the first row and first line
if not target_only:
feats = list(range(93))
else:
# TODO: Feature selection
# You can use the following code to get feature scores
feats = [75, 57, 42, 60, 78, 43, 61, 79, 40, 58, 76, 41, 59, 77, 92, 74, 56]
pass
if mode == 'test':
# Testing data
# data: 893 x 93 (40 states + day 1 (18) + day 2 (18) + day 3 (17))
data = data[:, feats]
self.data = torch.FloatTensor(data)
else:
# Training data (train/dev sets)
# data: 2700 x 94 (40 states + day 1 (18) + day 2 (18) + day 3 (18))
target = data[:, -1]
data = data[:, feats]
# Splitting training data into train & dev sets
if mode == 'train':
indices = [i for i in range(len(data)) if i % 10 != 0]
elif mode == 'dev':
indices = [i for i in range(len(data)) if i % 10 == 0]
# Convert data into PyTorch tensors
self.data = torch.FloatTensor(data[indices])
self.target = torch.FloatTensor(target[indices])
# Normalize features (you may remove this part to see what will happen)
self.data[:, 40:] = \
(self.data[:, 40:] - self.data[:, 40:].mean(dim=0, keepdim=True)) \
/ self.data[:, 40:].std(dim=0, keepdim=True)
self.dim = self.data.shape[1]
print('Finished reading the {} set of COVID19 Dataset ({} samples found, each dim = {})'
.format(mode, len(self.data), self.dim))
修改后,重新训练模型,提交结果发现效果并不好,我们暂时保留特征选择的结果,继续后面的操作。
2. DNN architecture 深度神经网络结构(layers? dimension? activation function?)
由于我们处理的数据量较小,所以我们使用的网络结构也应该相应的简单。
我们在原网络结构的基础上进行修改
修改思路如下:
-
不改变原有的层数,仍为两个全连接层
-
不改变激活函数ReLU,实测效果比Sigmod好
-
降低隐藏层维度,由原64变为现有的16,减少模型参数,降低计算开销,较大的隐藏层维度(64)增加了模型的复杂度和表达能力,但也可能更容易过拟合。
-
使用 Batch Normalization 对每一层的输出进行归一化,将其调整为均值为0、标准差为1。这可以缓解梯度消失和梯度爆炸的问题,通常会加快训练速度,并允许使用更高的学习率。
-
使用正则化技术 Dropout,用于在训练过程中随机“丢弃”一部分神经元的输出(在此例中是20%)。这样可以强迫模型的不同神经元学习到不同的特征,从而减少过拟合的可能性。
总的来说修改后的DNN architecture:
-
增加了
BatchNorm1d
和Dropout
,这两个层共同作用,增强了模型的训练稳定性和泛化能力,减少了过拟合的风险。 -
隐藏层维度更小,可能意味着模型更简单,计算更快,但表达能力可能有所限制。
class NeuralNet(nn.Module):
''' A simple fully-connected deep neural network '''
def __init__(self, input_dim):
super(NeuralNet, self).__init__()
# Define your neural network here
# TODO: How to modify this model to achieve better performance?
#self.net = nn.Sequential(
# nn.Linear(input_dim, 64),
# nn.ReLU(),
# nn.Linear(64, 1)
#)
self.net = nn.Sequential(
nn.Linear(input_dim, 16), # 输入层到第一个隐藏层,全连接层,输入维度为 input_dim,输出维度为 16
nn.BatchNorm1d(16), # 对第一个隐藏层的输出进行批量归一化,保持输出的均值为0,方差为1,防止梯度消失或爆炸
nn.Dropout(p=0.2), # Dropout层,在训练过程中随机将20%的神经元输出设为0,用于防止过拟合
nn.ReLU(), # 使用ReLU激活函数,将线性变换后的输出进行非线性映射,增加模型的表达能力
nn.Linear(16, 1) # 第二个全连接层,将16维的隐藏层输出转换为1维,作为最终的输出
)
# Mean squared error loss
self.criterion = nn.MSELoss(reduction='mean')
修改后重新训练模型,预测的结果明显较仅进行特征选择时变好,说明修改有积极作用,暂时保留。
此时的训练结果图像分析
3. Training Config 训练参数的调整
参考资料:
(mini-batch? optimizer? learning rate?)
slides提示我们,可以从mini-batch,optimizer,learning rate三个参数来入手
调整参数的对比分析:
n_epochs
-
前一次配置:
3000
-
后一次配置:
10000
将最大训练轮数从3000增加到10000,这意味着模型的训练时间将会更长。增加训练轮数可以让模型在复杂数据上有更多时间去学习,但也增加了过拟合的风险。需要注意的是,如果模型在较早的轮数中已经达到性能的瓶颈,继续训练可能不会带来显著的提升。
batch_size
-
前一次配置:
270
-
后一次配置:
200
将批量大小从270减少到200,使用较小的batch size通常可以带来更稳定的梯度更新,有时有助于提高模型的泛化性能。但也会增加训练时间,因为需要更多的批次来处理相同数量的数据。经过实践200为合适的批量大小。
optimizer
-
前一次配置:
SGD
-
后一次配置:
Adam
优化器从随机梯度下降(SGD)切换到自适应矩估计(Adam)。Adam通常比SGD有更快的收敛速度,因为它可以动态调整学习率,并且更好地处理稀疏梯度。然而,这也可能会导致模型陷入局部最优解。
optim_hparas
-
前一次配置:
lr = 0.001
-
后一次配置:
lr = 0.0005
学习率从0.001降低到0.0005,这在使用Adam优化器时是常见的做法,因为Adam本身具有动态调整学习率的机制。较低的学习率可以使模型更稳定地收敛,但也可能导致训练时间增加。移除了动量参数是因为Adam优化器本身带有类似动量的机制,不再需要显式指定。
early_stop
-
前一次配置:
200
-
后一次配置:
1000
早停策略从200个epoch增加到1000个epoch,这意味着给了模型更多的时间去寻找可能的性能提升。随着训练轮数的增加,适当延长早停的时间可以帮助捕捉到后期可能的小幅改进,但这也增加了训练时间。
# Setup Hyper-parameters
device = get_device() # get the current available device ('cpu' or 'cuda')
os.makedirs('models', exist_ok=True) # The trained model will be saved to ./models/
target_only = True # TODO: Using 40 states & 2 tested_positive features
# TODO: How to tune these hyper-parameters to improve your model's performance?
config = {
'n_epochs': 10000, # maximum number of epochs
'batch_size': 200, # mini-batch size for dataloader
'optimizer': 'Adam', # optimization algorithm (optimizer in torch.optim)
'optim_hparas': { # hyper-parameters for the optimizer (depends on which optimizer you are using)
'lr': 0.0005, # learning rate of SGD
#'momentum': 0.9 # momentum for SGD
},
'early_stop': 1000, # early stopping epochs (the number epochs since your model's last improvement)
'save_path': 'models/model.pth' # your model will be saved here
}
修改后重新训练模型,预测的结果明显变好,已经很接近strong baseline了,暂时保留修改。
此时的训练结果图像
4. L2 regularization L2 正则化
参考资料:一篇文章详解深度学习正则化方法(L1、L2、Dropout正则化相关概念、定义、数学公式、Python代码实现)-CSDN博客
L2 正则化:也称为 Ridge 正则化,它通过在模型的损失函数中增加权重的 L2 范数(权重向量的平方和)来实现正则化。L2 正则化会使权重值变得较小,但不会直接导致权重稀疏,因此不具有特征选择的作用,但可以有效地控制模型的复杂度。
使用L2正则项,尝试多组正则化参数,选取表现较好的一个(此外尝试过其他损失函数,效果与MSE相差不大)
def cal_loss(self, pred, target):
''' Calculate loss '''
regularization_loss = 0
for param in model.parameters():
# TODO: you may implement L1/L2 regularization here
regularization_loss += torch.sum(param ** 2)
return self.criterion(pred, target) + 0.00075 * regularization_loss
修改后重新训练模型,还未达到strong baseline,我们还需调整
训练结果的图像
暂时还未达到strong baseline
5. There are some mistakes in the sample code, can you find them?
我确实没找到这些mistakes,希望找到的朋友能在评论区留下答案。
Reference
【2】过private_strong_base_line记录 (kaggle.com)
【3】kaggle比赛中的private leaderboard和public leaderboard的区别_kaggle public和private-CSDN博客
【4】一篇文章详解深度学习正则化方法(L1、L2、Dropout正则化相关概念、定义、数学公式、Python代码实现)-CSDN博客