什么是Regression?

所谓的机器学习,其实就是一个找函数的过程。但是这个函数往往很复杂,靠人是找不出来的,要依靠机器采用某种方法才能找出这个函数。例如以下几种情况:

  • 音频识别:输入为一段音频信号,输出为这段音频的内容是什么?
  • 视频识别:输入为一张图片,输出为图片的内容是什么?

这节的Regression(回归分析),作为机器学习算法的一种,其面向于:函数的输出为一个数值(标量,scalar)

机器学习怎么玩?

进行机器学习一般的三步走:

  • 定义带有未知数的函数(Model)

    例如函数式y=wx+by=wx+b,y是待预测的值,x是已知值(feature),w(weight)和b(bias)是未知数

  • 定义损失函数L,L是关于w和b的函数(即输入是weight和bias)。L用于衡量当前设定的(weight,bias)这组数值好还是不好。L的计算来源于训练数据(Training Data)。

    在进行预测时,我们把预测值记为yy,把真实值记为y^\hat{y}(称为Label)。

    • 我们可以用yy^\lvert y-\hat{y}\rvert来表示预测与真实的差距,即L为mean absolute error(MAE)。
    • 也可以用(yy^)2(y-\hat{y})^2来表示,即L为mean square error(MSE)。
    • 这里的mean是指L应取所有训练数据的平均

    显然L越大表明当前参数(weight,bias)越烂;L越小就越好。

    我们可以穷举所有w和b的值,然后分别以w和b为x、y轴,L为指标绘图,就能直观看出不同的(w,b)对应的L怎么样,这样的图叫error surface

    image-20241130151202446
  • 优化(采用梯度下降法)

    w,b=argminw,bLw^*,b^*=arg\quad min_{w,b}\,L

    注:这里的argminarg\quad min意思是使得L最小时变量的取值;当然有argmaxarg\quad max就是最大时的取值

    下面说一下梯度下降的基本思路:

    • 简单起见,这里只考虑w参数

    • 首先随机选取一个w值,记为w0

    • 计算函数L在w0点的偏导Lww=w0\frac{\partial L}{\partial w}\rvert_{w=w_{0}}。如果偏导值为正,说明该点出的L曲线左低右高,要找L的最小值就要把w往左移;偏导为负同理。

    • w左右移动的距离,一方面取决于偏导的大小(偏导大肯定动的多),另一方面取决于我为其设定的超参数:学习率η\eta

      Δw=ηLww=w0\Delta w =\eta \cdot \frac{\partial L}{\partial w}\rvert_{w=w_{0}}

    • 那这样移动w到什么时候才结束呢?

      • 设定一个更新次数上限,比如更新1w次就不干了,当然这个次数也是一个超参数
      • 非常理 想的情况下,w左右移动的距离正好就是0,那w也不会再动了。这种情况下,就有可能陷入局部最优(local minima)

    同样的,当考虑上w和b两个参数时,情况就是这样的:

    image-20241130004540112

这三步走就构成了机器学习的**训练(Training)**过程。

我们的feature也可以取更多的值,如下图:

image-20241130151035087

形如这样的模型,我们称为线性模型(Linear Model)。很明显线性模型非常简单,没有办法模拟真实的情况。我们把线性模型与真实情况的差距称为Model的Bias(注意不是线性函数截距的那个bias哦)

进化版

一种简单的进化版就是用分段函数(Piecewise Linear Curve)

image-20241130160822760

而这条蓝色的折线,我们采用曲线Sigmoid Function来进行逼近,其曲线方程为

y=c11+e(b+wx1)=csigmoid(b+wx1)\begin{aligned} y&=c\frac{1}{1+e^{-(b+wx_{1})}}\\ &=c\cdot sigmoid(b+wx_{1}) \end{aligned}

显然该函数当x1+x_{1}\rightarrow+\infty时,函数趋向于常量c;当x1x_{1}\rightarrow-\infty时,函数趋向于0。

因此称那条蓝色的折线为Hard Sigmoid,可以通过选取不同的c、b、w的值来实现曲线向折线的逼近。总结一下,分段函数的表示记为:

y=b+icisigmoid(bi+wix1)y=b+\sum_{i}c_{i}sigmoid(b_{i}+w_{i}x_{1})

也就是说,icisigmoid(bi+wix1)\sum_{i}c_{i}sigmoid(b_{i}+w_{i}x_{1})这一项,其实是让原来的wx1wx_{1}更加细腻,更有弹性(难绷),如下图所示。显然这个sigmoidsigmoid函数项越多,能拟合的曲线就越曲折,具体要用多少个函数项也是一个超参数。

image-20241130221324692

当然我们可以把这些未知参数写成矩阵、向量的形式:cibic_{i}和b_{i}可以写作向量,wijw_{ij}可以写作一个矩阵。我们可以把所有的未知数都统一到向量θ\theta中,具体操作如下:

image-20241201203349733

到此,我们就完成了进化版的第一步:定义带有未知数的函数。

下面就要定义损失函数L了。L与$\theta 相关,即相关,即L(\theta)。选定一组。选定一组\theta$后,和原先一样,可以采用MAE或MSE来表示损失函数。

同样的,这里的优化梯度下降怎么go?

image-20241201205627811

但是,在实际操作中,我们不会拿所有的数据去算Loss,而是会把数据分成若干个Batch(随机分)(这里的Batch Size也是一个超参数),然后用每个Batch中的数据去计算,具体操作如下:

image-20241201211504985

这里的两个名词:

  • update:每次更新未知向量θ\theta叫一次update
  • epoch:完成所有Batch的一轮叫一个epoch

1epochupdate的次数=数据总量NBatch大小B1个epoch中update的次数=\frac{数据总量N}{Batch大小B}

激活函数(Activation Function):譬如这里的sigmoidsigmoid函数,就是我选用什么样的基本单元去拟合真实数据

还有神人操作:把sigmoid(bi+wix1)sigmoid(b_{i}+w_{i}x_{1})得到的结果再做一次sigmoidsigmoid运算,然后再做……具体要做几次(有几层Layer)?这又是一个超参数。如下图所示,注意每一次运算的参数(b、W这种)都不一样,相当于增加了更多的参数。

image-20241201213005207

我们把每一个基本运算(比如sigmoidReLUsigmoid、ReLU)称为神经元(Neuron),整个系统称为神经网络(Neural Network)。因为系统中有很多层(Layer),许多层构成深度(Deep),所以这种结构也称为深度学习(Deep Learning)

过拟合(Overfitting):在训练集上效果明明变好了,在测试集上却变烂了

image-20241201214507324

HM1

直接运行示例代码,结果在Private Score为1.61450,在Public Score上为1.56370。

第一次进化是考虑了feature的选择。

在函数select_feat(train_data, valid_data, test_data, config['select_all'])中,原本的情况是这样的:

1
2
3
if select_all:
# 如果 select_all 为 True,选择所有特征
feat_idx = list(range(raw_x_train.shape[1]))

此时feature的取值是训练集中的全部列,也就是把诸如hh_cmnty_cliwork_outside_home等参数全部考虑进去。一般而言,我只需要前4天的tested_positive参数放进去训练就行了,即下面这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
def getpositivecolumnnumber():
# 读取训练数据
train_data = pd.read_csv("./covid.train.csv")

# 统计包含 'tested_positive' 的列号
positivecolumnnumber = []
for i in range(len(train_data.columns)):
if "tested_positive" in train_data.columns[i]:
positivecolumnnumber.append(i)

#删去最后一个元素
positivecolumnnumber.pop()
return positivecolumnnumber
1
2
3
4
5
6
else:
# 否则,只选择指定的特征列(这里是第 0 到 4 列)
# feat_idx = [0, 1, 2, 3, 4] # TODO: Select suitable feature columns.
# feat_idx取所有tested_positive项对应的序号
print(getpositivecolumnnumber())
feat_idx = getpositivecolumnnumber()

这样子训练出的结果如下

image-20241202220004896

第二次进化是考虑模型架构(激活函数)和优化器选择。

优化器:How to choose optimizer ?训练时,如何选择优化器?_训练模型 更换优化器-CSDN博客

激活函数:神经网络中的激活函数及其各自的优缺点、以及如何选择激活函数_神经网络的激活函数有哪些?他们对神经网络的性能有何影响。-CSDN博客

对于优化器而言,原代码选取的是SGD

1
optimizer = torch.optim.SGD(model.parameters(), lr=config['learning_rate'], momentum=0.9)

先是更换了Adagrad优化器

1
optimizer = torch.optim.Adagrad(model.parameters(), lr=config['learning_rate'])

但是效果依托,Loss始终居高不下;听从AI建议选取Adam优化器,训练后结果如下:

image-20241202221800605

发现在训练过程中,Loss减小非常缓慢,就加大了学习率为'learning_rate': 1e-4,再次训练后的结果:

image-20241202221943280

效果还是有的!后续再提高一些学习率,效果有微小的提升,但是好像是摸到了这个优化器的瓶颈。

对于激活函数选择方面,原代码中是采用ReLU和2层神经元:

1
2
3
4
5
6
7
self.layers = nn.Sequential(
nn.Linear(input_dim, 16),
nn.ReLU(),
nn.Linear(16, 8),
nn.ReLU(),
nn.Linear(8, 1)
)

先是尝试了增加神经元层数到3层,也增加了神经元数量:

1
2
3
4
5
6
7
8
9
10
11
self.layers = nn.Sequential(
nn.Linear(input_dim, 64), # 增加第一层的神经元数量
nn.ReLU(),
nn.Dropout(0.5), # 添加 Dropout 层,以减少过拟合的风险。
nn.Linear(64, 32), # 增加第二层的神经元数量
nn.ReLU(),
nn.Dropout(0.5), # 添加 Dropout 层
nn.Linear(32, 16), # 增加第三层
nn.ReLU(),
nn.Linear(16, 1) # 输出层
)

但是效果不佳。听从AI建议选用了LeakyReLUELU激活函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 换用LeakyReLU激活函数
self.layers = nn.Sequential(
nn.Linear(input_dim, 16),
nn.LeakyReLU(),
nn.Linear(16, 8),
nn.LeakyReLU(),
nn.Linear(8, 1)
)

#换用ELU激活函数
self.layers = nn.Sequential(
nn.Linear(input_dim, 16),
nn.ELU(),
nn.Linear(16, 8),
nn.ELU(),
nn.Linear(8, 1)
)

LeakyReLU

image-20241203154412870

ELU

image-20241203154759209

第三次进化考虑的就多了。

  • 之前的特征选择只有前4天的tested_positive参数,现在希望考虑更多参数(考虑“影响力更大”的参数)
  • 为了防止过拟合,可以使用L2正规化或者增加Dropout层
  • 学习率可以在训练过程中动态变化(逐渐降低),防止在目标点反复横跳。

首先是做了第一条:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 使用 sklearn 中的特征选择方法选择最有影响的特征

from sklearn.feature_selection import SelectKBest, f_regression
from sklearn.preprocessing import MinMaxScaler

def select_features(X_train, y_train, X_valid, X_test, k=24):
selector = SelectKBest(score_func=f_regression, k=k)
X_train_selected = selector.fit_transform(X_train, y_train)
X_valid_selected = selector.transform(X_valid)
X_test_selected = selector.transform(X_test)
return X_train_selected, X_valid_selected, X_test_selected

x_train, x_valid, x_test = select_features(x_train, y_train, x_valid, x_test, k=24)
# 特征归一化
scaler = MinMaxScaler()
x_train = scaler.fit_transform(x_train)
x_valid = scaler.transform(x_valid)
x_test = scaler.transform(x_test)

即选取了24个最有“代表性”的数据作为feature,结果是有进步的:

image-20241203161604724

第二条:

1
2
3
4
# 优化器更换为Adam
optimizer = torch.optim.Adam(
model.parameters(), lr=config["learning_rate"], weight_decay=0.05
) # weight_decay参数为L2正则化项的系数,即权重衰减系数

这个挺有用的,但是在改进模型架构时,并没有非常好的效果:

1
2
3
4
5
6
7
8
9
10
11
12
# 换用LeakyReLU激活函数
self.layers = nn.Sequential(
nn.Linear(input_dim, 16),
nn.LeakyReLU(0.1),
nn.Dropout(0.5),

nn.Linear(16, 8),
nn.LeakyReLU(0.1),
nn.Dropout(0.5),

nn.Linear(8, 1)
)

第三条加上去后效果也一般:

1
2
# 学习率调度器更换为余弦退火调度器
scheduler = CosineAnnealingLR(optimizer, T_max=config["n_epochs"])

后续再改进,当前的最好结果是:

image-20241203211104004