基于李宏毅机器学习课程(22年春)所写的课程笔记

课程主页:ML 2022 Spring

此外,李老师在25年春开设新机器学习课程,主要内容为LLM及相关技术,课程主页为ML 2025 Spring

Regression回归分析

什么是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

Classification分类

这节的Classification分类,指的是函数的输出是从给定的几个选项中选取的。

如何做好机器学习?

image-20241203211157598

首先需要判断在训练集上的Loss大小:

  • 如果训练集Loss偏大,那就是Model Bias或者优化的问题

    • Model Bias:模型太过简单,能使得Loss小的Function根本就不在如今的Function集中。解决这个问题说白了就是找更复杂的模型,用更多的feature(比如预测阳性率可以拿前10天的阳性率而不是前1天的阳性率)
    • 优化:由于优化用的都是梯度下降,可能会进到局部最优。
    • 怎么样去分辨到底是模型烂还是优化没做好:可以先选一个简单的模型去看看它解决问题的Loss应该是多少;然后再去训练复杂模型,如果复杂模型的Loss比简单模型的还大,那基本上就是优化没做好了。
  • 如果训练集Loss很小,符合要求的。测试集Loss也很小,那就ok了;如果测试集Loss比较大,就可能是过拟合或者**不匹配(mismatch)**的问题。

    • 过拟合:在训练集上Loss小,但在测试集上Loss大。

      • 处理过拟合问题最好的办法就是增加数据集

      • 此外还可以做**数据增强(Data Augmentation)**去创造新的数据(譬如在图像识别中把原图像左右翻转、变色、截取得到的图片拿去训练)

        注意:这些新的数据必须符合原数据集的规律

        image-20241203212959397

        比如这里把猫上下翻转就不应该作为“创造”的数据,因为现实生活没有这样的图片,数据集中也不可能有这样的图片,放这种图片只会让机器学到奇葩结果。

      • 还有方法就是限制Function(Constrained Function)。因为训练数据不够时,拟合出的Function可能会在两个数据点之间整花活:

        image-20241203213519243

        我们通过对Function的约束来保证他不会整花活。比如这里的数据,我们适合用二次函数去拟合它,而不是用五次、六次函数去拟合。二次函数的参数更少,约束程度越高。但是这个约束也不能太强,比如拿一次函数去拟合,就必然结果依托。

    • 不匹配(mismatch):**训练集和测试集的数据分布不一样。**比如我解决一个播放量预测的问题,在测试集上的结果是周五和周六播放量最低;但是由于测试集中某个周五由于某个特殊原因播放量暴增(类似于极端情况,很少发生的那种),我的预测显然就不可能准的起来。在面临mismatch时,一昧增加训练集已经没用了。还有一个例子如下:

      image-20241203221954334

由此我们可以发现,当模型的复杂程度越高(feature越多,参数越多),其在训练集和测试集上的结果可能是这样的:

image-20241203215042247

优化阶段如何做好梯度下降?

在训练过程中,训练集Loss可能出现以下两种情况:

  • Loss确实随着训练的进行而逐步下降,但是其最后稳定的值还不够小
  • Loss从一开始训练就没变过,一直很烂

出现这种情况一般认为是训练卡在了梯度为0的点。这种点可以是局部最小值 (local minima) 或鞍点(saddle point),统称为驻点(Critical Point)

image-20241204143632057

**如何判断到底是卡在了局部最小还是鞍点呢?**因为对于局部最小而言,卡住了就真的寄了(因为其往四周Loss都会变大);但是对于鞍点而言,其实还有路可走。

我们一般采用黑塞矩阵来进行判断:

image-20241204153224748

事实上,在低维空间中的局部最小点,在高维空间中就可能变成鞍点。

image-20241204153340805

Batch(批次)的选择依据?

image-20241201211504985

在训练过程中,Loss函数的计算需要去使用训练集中的数据。在实际操作过程中,我们会把所有的训练数据分为若干batch,每个batch代入后算出的Loss函数不同,进而算出的梯度不同。

image-20241204164150364

在上图中,我们考虑训练集中共有20个数据。

  • 左图中Batch Size=20,相当于没有分batch,这种情况称为Full Batch。在该情况下,Loss函数的计算需要使用这全部20个数据,然后算一次梯度,参数就只变一次。这种方法蓄力时间比较长,但是它的变化比较可靠。
  • 右图中Batch Size=1,相当于分了20个Batch。在这种情况下,只需要拿一个数据就能计算Loss函数,进而计算梯度,整体下来能变更20次参数。从图中可以看出,由于只参考一个数据,noise比较多,其变化是比较杂乱无章的。这种方法蓄力短,能调很多次,但是噪声多,调节不可靠

在实际使用中,由于Loss计算的每项可以在GPU中并行,所以Batch的Size对运行时间的影响并不是很大(除非Batch Size巨大无比,超过了GPU的并行能力)。

正因为这样,右图虽然每次Update耗费时间短(其实也没比左图的Update短多少),但是右图的一个epoch要做的Update就多得多了。每个Update是在上一个Update的基础上做的,是没有办法并行的。这样看来,同样完成一个epoch,左图的时间反而比右图少。

image-20241204165727281

然而,小的Batch Size所产生的Noise能力反而为它赋予了更好的优化性能(测试集)和泛化能力(训练集)。

  • 训练集上,由于每个Batch都能对应一个Loss函数。我在L1L^1损失函数上陷入驻点后;可能在L2L^2损失函数上又能接着梯度下降。

    image-20241204172051350
  • 测试集上,小的Batch Size更倾向于找到的最小值为Flat Minima(由于其Noise特性让其能够跳出Sharp Minima点)。当测试集由于mismatch等问题与训练集的Loss函数不匹配时,Flat Minima能够保证Testing Loss与Training Loss相差不大。

    image-20241204172524727

此外,为了解决卡比在驻点的这种问题,还可能采用**动量 (momentum)**这种技术。想象损失函数是一个物理层面的斜坡,目标点是斜坡上的一个小球。当小球滚动时,其可能会滚到一个低谷(局部最小点),但是由于其拥有惯性(动量),还可以再冲一下,就有可能跳出这个Local Minima。

传统的梯度下降是这样的:每次变更参数的方向都是梯度的反方向

image-20241204173911640

带有动量的梯度下降是这样的:每次变更参数的方向是梯度的反方向与上次移动方向的矢量和

image-20241204174035190

学习率如何自动调整?

当我们的Training Loss不断减小,直至稳定到较小值后,我们还需要明确此时的梯度值是否真的减小了?如果梯度值确实变小了,说明可能进入了驻点;如果梯度值仍然很大,就仍然是优化的问题。

对于学习率而言,

  • 如果学习率设定值太大,就可能会反复横跳。
  • 如果学习率设定值太小,就动的太慢,到不了极小值点。
  • 因此最好我们能够根据该点处的梯度情况来动态决定学习率。梯度小(平坦)时,学习率应该大一些;梯度大(陡峭)时,学习率应该小一点。
image-20241204195012557

在这里我们修改了学习率的表示,在这里的σit\sigma_{i}^{t}代表新的学习率与ii(哪个参数)和tt(第几轮的Update)相关。

一种σit\sigma_{i}^{t}的计算方式是Root Mean Square

image-20241204195523493

另一种计算方法是RMSProp:使用α\alpha参数来提升当前梯度的重要性

image-20241204200446128

如此这般又会产生新问题:如果一连好多个Update的梯度值都特别小,就会导致σit\sigma_{i}^{t}变得特别小。这时如果突然遇到一个梯度的微增,θ\theta的移动就会直接喷薄而出。

image-20241204202755139

为了解决这个问题,我们会让学习率η\eta也变成轮数tt的函数,即ηt\eta^t。这个函数可以是这样的:

  • Learning Rate Decay:随轮数变大,学习率逐渐减小

    image-20241204203005019
  • Warm up

    image-20241204203027667

如何做分类问题?

进行分类问题时,我们一般会用one-hot vector来表示每个分类情况:

image-20241204211203932

在处理Regression问题时,我们的输出是一个标量值,但是在分类问题中,我们的输出应该是一个向量。该向量中元素的获取方式与Regression问题实际上是一致的:

image-20241204211423393

但是这里得到的y1、y2、y3并不能直接和真实值间作用,还需要经过softmax处理:

image-20241204211713798 image-20241204211540881

在上图中,y的各元素取值可以是任意值(正负都可以),而y’则是0到1的值。

在计算Loss时,可以使用MSE,但对于分类问题而言,更常用的损失函数是Cross-entropy

image-20241204211857423

HM2

在这次训练里,参数有一个acc(准确率),正因分类问题可以区分分类的对不对,所以会有这个值。它的定义方式是这样的:acc=正确预测的样本数/总样本数。acc和loss值不一样哦!例如在分类问题中的loss值是通过Cross-entropy得到的。

尽管准确率和损失函数都用于评估模型的性能,但它们之间并不完全等价。准确率只关心预测是否正确,而损失函数则关注预测的概率值。

直接运行示例代码的结果如下:

image-20241208193015674

观察其输出的训练集与测试集acc和loss,发现训练集的loss就很高,acc很差。

Medium baseline

首先是认为模型太过简单,我们应该考虑更多的feather。这个与参数concat_nframes相关,该参数原本的取值为1,说明其没有考虑该音素frame的前后项。换了一个大的concat_nframes,注意该参数需要是奇数。

1
2
3
4
# concat_nframes这个参数代表了我们要将多少帧的特征拼接在一起
#这个参数越大,模型的感受野就越大,但是也会增加模型的复杂度

concat_nframes = 19 # the number of frames to concat with, n must be odd (total 2k+1 = n frames)

image-20241208193834386

image-20241208193914975

后续是将学习率调高,模型架构改的更宽(hidden_dim)更深(hidden_layers)

1
2
3
4
5
6
7
8
9
10
class Classifier(nn.Module):
def __init__(self, input_dim, output_dim=41, hidden_layers=5, hidden_dim=512):
super(Classifier, self).__init__()

self.fc = nn.Sequential(
BasicBlock(input_dim, hidden_dim),
*[BasicBlock(hidden_dim, hidden_dim) for _ in range(hidden_layers)],#堆叠hidden_layers个BasicBlock,提高深度
nn.Linear(hidden_dim, output_dim)
)

image-20241208201728137

Strong baseline

首先是进行了批量规范化(batchnorm),为基本模型添加了一个规范化层:

1
2
3
4
5
6
7
8
9
class BasicBlock(nn.Module):
def __init__(self, input_dim, output_dim):
super(BasicBlock, self).__init__()

self.block = nn.Sequential(
nn.Linear(input_dim, output_dim),
nn.BatchNorm1d(output_dim),#规范化层
nn.ReLU(),
)

image-20241208210812293

随后调小了batch_size,增加了模型宽度,采用了Learning Rate Decay的学习率动态变化策略。

image-20241209152111496

后续采用了Learning Rate Decay和Warm Up的综合方式,但可能由于epoch太少,似乎进到了过拟合。

image-20241209153758350

CNN卷积神经网络

CNN属于网络架构的一种,其一般被用于影像领域。

如何去做图像分类

图片在计算机中的存储通常是以3维张量来进行的。例如一张图片是100像素*100像素,每个像素由RGB3个channel组成(这个数值代表这个颜色的强度);那么要表示这张图片就需要31001003\cdot100\cdot100个数据。我们将这些数据做成应该向量作为神经网络的输入。

那么就会面临一个问题:例如第一层的神经元共1000个,输入向量为31001003\cdot100\cdot100个,那么光第一层所需要的weight数量就高达31001001000=31073\cdot100\cdot100\cdot1000=3\cdot10^7个,这样会拖慢训练进度,还容易造成过拟合。

因此我们需要进行优化:

  • 和人眼一样,图片分类时机器也只需要关注这张图片中最关键的部分就可以了。因此每个神经元的输入不需要是完整的图片。

    image-20241206183628370

    我们为每个神经元分配一个Receptive field,该神经元只需要输入该Receptive field中的数据即可。Receptive field之间可以重叠,甚至可以多个神经元共用一个Receptive field。

    在设计Receptive field时,其可以有不同的大小(不一定要是3×33\times3的,n×nn\times n的都行);可以只看一个channel(比如只看RED);也可以是长方形;甚至不相连都行,关键是要看图片是啥样的,图片的关键在哪里。

    经典的设计方法是这样的:

    • 使用全部的channel,field大小一般取正方形(3×33\times 3比如)

    • 每个Receptive field对应多个神经元(例如64个、128个这种)

    • 每两个Receptive field间有重叠部分,以最左上角的Receptive field为基准,向右或向下移动stride个单位(stride一般取1或2,也是一个超参数)。如此以保证当图像特征出现在中间时能有神经元关注这一块。

    • 由于是3×33\times 3,所以肯定会有突出的几块,这些采用padding进行处理,例如取0、取平均这种

      image-20241207201249863
  • 对不同的图片而言,虽然是相同的特征,但是该特征分布在两张图片的不同位置,而这两种位置会对应两组不同的神经元。这两组神经元的参数是相似的,因此可以进行参数共享来减小总参数量。

    image-20241207201937248

    上图中的两个神经元,其weight参数完全一样。

    当然,如果Receptive field相同的神经元之间没必要共享参数,因为那样搞的话输出都是一样的了。

    经典的设计方法是这样的:

    image-20241207202644836

    这里颜色相同的神经元代表共享参数的神经元,依次记作filter xx这样子。

CNN的训练流程是什么样的?

image-20241207205558864

Convolution卷积层:假设我们已经通过梯度下降得到了每一个神经元filter的权重。然后将filter的权重与图片数据各项相乘并相加。

image-20241208161450021

在上图中,filter的斜线上全是1,就代表在图片上扫的时候,碰到同样的形状时,产生的值最大。

然后,对于每一个filter都搞相同的操作:

image-20241208164505085

上图中,有几个filter就代表会有多少层这样的结果数据。我们把这些结果数据称为Feather Map。假设我们这一层卷积层有64个神经元,那么产生的Feather Map就会有64个channel,我们可以把这个Feather Map也看作是一个“图片”。

我们可以把这个Feather Map再扔进新的卷积层中,让这个新“图片”再和新的filter作用,注意这里的filter的channel数应该与Feather Map的channel数相同。

Pooling层:我们对原来的图片进行抽样,例如取图片中所有的偶数行、奇数列这样子,图片大小变为原来的14\frac{1}{4},但是并不会影响识别结果。

在做Pooling时,我们会将由每一个filter产生的数字几个几个分组:

image-20241208181327023

然后我们就需要在每个分组中选一个“代表”来表示这一组。以Max Pooling这种方法为例,就会取每个分组中的最大值(当然也会有Average Pooling、Min Pooling这种)

image-20241208181517793

我们通常会在卷积层后使用Pooling(交替使用),可以缩小处理数据的大小:

image-20241208181647391

当然,Pooling会造成信息损失,并不是必要的。如果要侦测微小的特征或者算力够够,不加Pooling是应该的。

HM3

Medium

首先给训练管道加上了数据增强操作(这里要注意不能增强过度),然后将训练epoch加了一点。

1
2
3
4
5
6
7
8
9
10
11
train_tfm = transforms.Compose(
[
transforms.Resize((128, 128)),
transforms.RandomHorizontalFlip(p=0.5),#随机水平翻转
transforms.RandomRotation(degrees=30),#随机旋转
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),#随机颜色变换
transforms.RandomResizedCrop(size=(128, 128), scale=(0.8, 1.0)),#随机裁剪
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),#标准化
]
)

Self-attention自注意力机制

什么是Self-attention

Self-attention用于解决输入是许多向量的情况(向量个数不固定),即sequence。比如在NLP中,我们的输入是一个句子,把句子中的每一个单词看成一个向量,那么这个句子就是诸多向量的集合;并且由于句子长度会变化,这个集合的大小也会变。

这种模型输出可能有3种:

  • 每个向量都对应一个输出,输入和输出的长度一致。每个输出可以是数值(Regression),可以是类别(Classification)。这种输出模式称为Sequence Labeling

    image-20241219222131885

    比如说NLP中的词性标注,每一个单词都对应一个词性就适用这种输出。

  • 整个向量集合就一个输出。

    image-20241219222458878

    比如去判断某句话是positive还是negative就适用这种输出。

  • 输出数由模型自己决定,这种任务即seq2seq的任务。

    image-20241219230444412

    比如翻译任务,输入和输出是不同的语言,就属于seq2seq的任务。

Self-attention怎么架构?

下面的介绍都以Sequence Labeling为例。

总的来说

image-20241219232459383

这里的Self-attention层需要考虑输入的全部向量,并且对于每个向量都给出相对应的一个输出,让每个输出经过一个全连接层得到最后的label。

当然这里的Self-attention层也可以叠很多层,与全连接层交替使用:

image-20241219232753874

具体每个输出的向量怎么来?

image-20241219233215994

这里的b1、b2、b3、b4都需要考虑a1、a2、a3、a4这4个向量才能生成,下面来具体叙述一下生成过程(以b1为例):当然这里b1、b2、b3、b4都是并行生成的,它们之间没有依赖关系。

  • 衡量a1向量与其他向量间的相关度α\alpha。这个的计算方法有很多种,下面列出了两种:

    image-20241219233749464

    这里的WqW^qWkW^k代表两个不同的矩阵,它们是神经网络的参数。在Dot-product方法里,待计算相关度的两个向量分别与这两个矩阵相乘得到向量qqkk,把向量qqkk各项相乘再相加就得到相关度α\alpha

    image-20241220002302572

    这里也不一定要经过Soft-max层,可以是ReLU等等函数。这层的作用实际上是在正规化,让正规化后的四个值相加为1即可。

  • 根据attention score抽取每个输出向量的关键部分

    image-20241220002616854

以矩阵的形式来表示如下:

image-20241220162525125 image-20241220163015863 image-20241220163156685

在整个训练过程中,只有矩阵WqW^qWkW^kWvW^v三个的数值是需要我们通过数据集去找出的。

Multi-head Self-attention

两个向量之间的“相关”可能不单单是一个层面上的,可以有多个方面的相关,Multi-head Self-attention就用于处理这种情况。以2-head为例,

image-20241220164528073

在得到qiq^i后,把qiq^i乘上另外两个矩阵,得到qi,1q^{i,1}qi,2q^{i,2}(对kik^iviv^i的处理一致)。然后对同为1或者2的q、k、v做dot product和相加这种操作,最后得到的向量是bi,1b^{i,1}bi,2b^{i,2}这两个。把这两个向量接起来,再做一个变换变成b向量。

image-20241221184657867

这里的head数也是一个超参数。

考虑上位置因素?

在前面的所有讨论中,每个输入向量不论处于哪个位置,对该向量的处理都是一样的,没有涉及到“这个向量在哪里”这个问题。

比如在词性辨识中,第一个单词是助词的概率很小,而是名词形容词的概率就比较大,我们需要考虑上这一点。

考虑位置的操作即Positional Encoding,是采用往aia^i上加上一个新向量eie^i来实现的。而这个新向量eie^i可以通过人工去设定,也可以从数据集中学到。

image-20241221195200876

自注意力机制不仅仅适用于NLP

语音数据:Truncated Self-attention

当我们在做语音辨识时,由于音频所产生的向量数非常多,就会导致矩阵WqW^qWkW^kWvW^v非常大以至于存不进内存。在这种情况下我们会考虑Truncated Self-attention这种方法,即每个Self-attention层不是考虑全部的输入向量,而是考虑一部分输入向量。

例如对于输出向量b3b^3而言,其需要考虑的输入向量是a3a^3及其前后几个向量,至于具体是几个那这也是一个超参数。

图片数据如何Self-attention?

image-20241221203324786

我们将每个像素点上RGB三个channel的值作为输入的向量。

可以比较一下处理图片时的CNN和Self-attention:

  • CNN只考虑Receptive field中的像素点(比如3*3大小)。
  • Self-attention会涉及到图片中任意两个像素点间向量的dot product,利用attention去找出相关性,相当于Receptive field是被“学”出来的而不是人为设定的,因此实际上是考虑了全图。
  • 由此可见CNN是简化版的Self-attention。而由于Self-attention更加灵活,其训练需要的数据量会更大。(训练数据少会过拟合)

同样是处理序列数据,也可以比较一下RNN和Self-attention:

image-20241221204726070
  • 对于RNN末尾的输出而言,其想要考虑到输入的第一个向量是比较困难的,因为它要把初始向量存在memory里一直带到最后。
  • RNN的后项输出依赖前项的输出,不能像Self-attention一样进行并行处理。

Self-attention处理Graph

在Graph中,每个Node是Self-attention中的输入向量,每个Edge能代表两个Node间的相关性。因此在使用Self-attention时,在计算Attention Matrix时,可以只计算有Edge连接的两个Node向量间的相关性,其余直接设0即可。这种网络架构即GNN(图神经网络)

image-20241221211402712

Transformer架构

Transformer与self-attention间的关系

Transformer 是一种基于 self-attention 机制的深度学习架构,主要用于自然语言处理任务。Transformer 的典型结构包括编码器(Encoder)和解码器(Decoder),其中每个编码器和解码器层都使用了 self-attention 机制。

使用 self-attention 的 Transformer 模型可以比传统的递归神经网络(RNN)并行计算,更加高效,并且能够捕捉到丰富的上下文关系。

什么是Transformer?

Transformer是一种处理seq2seq的模型,并且输出的seq长度未知,由机器自己决定。常用的应用有语音转文字、机器翻译这种

Transformer架构的基本组成:

image-20250110233040630

Encoder

对于编码器而言,其目标是输入一串向量,输出为同样长度的一串向量,能做到这件事的有很多种方式,比如RNN、CNN、self-attention等。在Encoder中,我们使用的是self-attention。


Encoder的基本结构是这样的:

image-20250115150419280

这里的每一个Block里都有多个layer,在transformer中每个Block的具体实现如下:

image-20250115154051379

具体而言是这样的:

image-20250115151036941
  • 每个self-attention层的输出变成了其原先的输出+输入,这种操作称为残差连接(residual connection),随后把这个新输出进行一遍layer norm(与batch norm不同),这样得到的输出导入下面的FC层
  • 在FC层中,同样要进行残差连接和layer norm,最后得到的输出才是该Block的输出

当然上述Encoder的设计是最原版的,并不一定是最优的

Decoder

自回归解码器 Autoregressive-Decoder

首先,对于Decoder的输出而言,首先要确定一个词汇表:

image-20250116135336493
  • 该词汇表是一个长度为V的向量,各元素是我想要输出的东西,右边的数字是每个元素出现的概率。这些数字是经过softmax的,所以其和为1。

    当然概率最大的元素就是最终的输出。

  • 例如要输出的是中文,那这个向量可以是全部的中文字

  • 如果输出的是英文,那这个向量可以是字母、单词、subword等

image-20250116140530631

在运行过程中,首先Encoder会通过某种方式将其输出加载进Decoder中。然后Decoder会被输入一个token叫BEGIN,这个BEGIN代表开始功能。随后,有BEGIN导出的第一个输出又会作为输入:

image-20250116140750904

此时的输入是BEGIN和上一次的输出

循环往复做下去,最终达到的结果是这样的:

image-20250116140956595

下面叙述一下Decoder内部在干什么:

image-20250116225429

实际上Decoder在做的事和Encoder很类似,除了这里的self-attention层用的是masked版本。对于Decoder而言,其某个向量的输出只需要考虑该向量的输入和在该向量之前的所有向量的输入,不需要考虑所有输入向量(其实也没法考虑)


Decoder是如何确定自己的输出长度是多少的?

如果始终保持上面提到的算法,那么在输入“习”字以后,又会产生新的输出;这样就会根本停不下来。为了解决这个问题,我们需要在原来的词汇表中增加一个END字符来表示输出结束。

image-20250117001036346

当输入“习”以后,其输出中END字符的概率应该最大。

非自回归解码 Non-autoregressive Decoder

image-20250117001338578

NAT的Decoder是将一排BEGIN作为输入,一步到位生成输出的句子。


既然AT是通过END字符来控制输出的长度,那NAT是通过什么方式来控制的?

  • 一种方式是再加一个预测器,这个预测器的输入是Encoder的输出,输出是预测的Decoder的输出长度

  • 另一种方式是:首先确定可能是输出最长长度(比如说N),那我就只需要输入N个BEGIN即可。随后观察到哪个BEGIN的输出是END,就把END以后的输出都丢弃即可。

    image-20250117002201609

NAT相比AT而言,有如下优势:

  • AT是后一个的输入依赖前一个的输出,而NAT则不需要,因此可以并行执行
  • NAT还可以方便地控制输出的长度

交叉注意力Cross Attention

Cross Attention是连接Encoder和Decoder的桥梁,下面介绍Encoder的输出是如何传递到Decoder中去的。

image-20250117002845148 image-20250117002916402

在最原版的transformer中,Decoder的每一层所用到的都是Encoder的最终输出。实际上各种变体还是很多的,比如Decoder每层看Encoder对应层、Decoder第一层看Encoder最后一层,Decoder第二层看Encoder倒数第二层······。

如何训练Transformer

transformer的训练实际上与分类问题一致。每一个输出的汉字都是一个one-hot的向量,我们的目标是让这个one-hot向量和Decoder输出的概率分布向量的cross entropy最小。

image-20250117005703194

当多个向量输入时,我们就要使得所有向量的cross entropy的总和最小。

要注意还有END这个特殊字符

image-20250117005911858

在这里Decoder的输入是“正确答案”,这种训练方式叫Teacher forcing

GAN生成式对抗网络

什么是生成式

所有的网络功能可以分为两种,一种是判别式的,给定一个输入x,其会输出一个固定的y;另一种是生成式的,此时的输入除了x,还会有一个随机性的z:

image-20250117133835580

  • 在这里z的分布应当比较简单(可以用函数式来表示)
  • 由于z随机,输出的y也具有随机性,可以看作是一个复杂的分布
  • 我们把这样的网络称为Generator生成器。

这样的网络是为了解决这种情况:同样的输入应该有不同的输出,而这些不同的输出都是正确的。或者说这个模型是带点创造力在的。


Unconditional generation:当不考虑输入x,仅考虑输入是分布z时的情况。

下面来解决二次元头像生成这个问题,我们选取的Z分布是一个正态分布(当然选其他的分布也可以,只要够简单能够用函数式来表示即可)

我们的目标是:从Z分布中sample出低维(50-100)的向量,这个向量经过Generator能够变成图片(高维向量)

在该任务中,我们除了要设计Generator,还要设计一个判别器(Discriminator)。判别器也是一个神经网络,在这个任务中其作用是判断一张图片像不像是二次元图片(即输入为一张图片,输出为一个数值,数值越大代表越像)

在GAN中的“对抗”其实就体现在这。生成器负责生成图像,而判别器负责判断生成的图像是真实的还是伪造的。两者通过对抗训练进行更新,生成器试图生成越来越真实的图像,以欺骗判别器。

生成器与判别器算法

首先要初始化Generator和Discriminator的参数,随后在每个训练阶段都要干这些事:

  • 固定Generator的所有参数,把一堆从分布Z中sample出来的向量塞进Generator,就会吐出一堆图片(一开始这些图片类似于杂讯)

    然后从二次元图库数据集中也sample出几张图片,然后就拿真正的二次元图片和Generator产生出的结果去训练Discriminator,目的是为了让Discriminator能够分辨真正的二次元图片和假的二次元图片。这样的操作可以是一个分类模型(真实图片为1,假图片为0),也可以是一个回归问题。

    image-20250119200111081

  • 固定Discriminator的所有参数,开始训练Generator。

    把某个向量丢进Generator,其得到的图片再丢进Discriminator,就能得到一个数值。而我们训练Generator的目的就在于让这个数值变大。

  • 依照以上操作反复执行,不断更新Generator和Discriminator的参数