MLP 多层感知机+权重衰减+L1L2范数+激活函数
为了拟合更特殊的函数,在网络中加入多个隐藏层,克服线性的限制。最后一层可以看作线性predictor。
一、
1.最简单流程
输入x矩阵,含有n个样本,每个样本有d个特征。经过隐藏层H将维度转化为h,在经过最后的输出层O将维度转化为q。
2.当我们添加了多个隐藏层时,如果只是对上一层的输出做一个简单的映射,可以发现:
合并隐藏层后其实等价于单层的模型。
所以,需要在每个隐藏单元输出应用激活函数σ(常用包括 relu 0~1,sigmoid 0~1,tanh -1~1)
作用1:它决定了节点是否应该被激活(即,是否让信息通过该节点继续在网络中向后传播),这样就避免了上述的退化情况。
作用2:把当前特征空间通过一定的线性映射转换到另一个空间,让数据能够更好地被分类。
3.如果是全连接的网络,每个神经元都依赖于所有输入的值。所以理论上只有一个隐藏层也可以通过足够的神经元和权重,拟合任意函数。
不过,使用更深(而不是更广)的网络,可以更容易的拟合函数。
4.代码实现
· 初始化w、b
· def relu(X):
a = torch.zeros_like(X) # 创建一个与X形状相同且元素全为0的张量
return torch.max(X, a)
· def net(X):
X = X.reshape((-1, num_inputs)) # 将每张图片都拉平成一个一维的向量
H = relu(X@W1 + b1) # 这里“@”代表矩阵乘法
return (H@W2 + b2)
· loss = nn.CrossEntropyLoss(reduction='none')
· updater = torch.optim.SGD(params, lr=lr)
· d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)
5.总结
· 对于相同的分类问题,多层感知机的实现与softmax回归的实现相同,只是多层感知机的实现里增加了带有激活函数的隐藏层。
· 不同的层数、激活函数、权重,都会影响模型acc。
二、过拟合 欠拟合
1.在监督学习中(有监督学习指的是 我们知道每个样本的结果 如回归,无监督学习指的是 不知道/没有样本的结果 如聚类降维),我们假设train set和test set是独立同分布的。
2.介绍几个倾向于影响模型泛化的因素
· 可调整参数的数量。当可调整参数的数量(有时称为自由度)很大时,模型往往更容易过拟合。因为容易受到噪声的影响而拟合歪了。
· 参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。
· 训练样本的数量。即使模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。
即:数据越大,参数(权重)越小,模型越简单,就越不过拟合。反之欠拟合。
蓝色过拟合
3.验证集
实际应用中,测试集只会使用一次。所以我们会通过验证集确定一个最好的超参数,最后再测试。
我记得验证集是从训练集里分出来的,测试集是单独的。
4.K折交叉验证
当训练数据稀缺时,我们甚至可能无法提供足够的数据来构成一个合适的验证集。这个问题的一个流行的解决方案是采用K折交叉验证。
这里,原始训练数据被分成K个不重叠的子集。然后执行K次模型训练和验证,每次在K-1个子集上进行训练,并在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证。最后,通过对K次实验的结果取平均来估计训练和验证误差。
5.生成数据集的代码
features = np.random.normal(size=(n_train + n_test, 1)) # 生成特征
np.random.shuffle(features)
poly_features = np.power(features, np.arange(max_degree).reshape(1, -1)) # 多项式特征
for i in range(max_degree): # gamma函数重新缩放
poly_features[:, i] /= math.gamma(i + 1) # gamma(n)=(n-1)!
# labels的维度:(n_train+n_test,)
labels = np.dot(poly_features, true_w) #点乘
labels += np.random.normal(scale=0.1, size=labels.shape) # 添加噪声
当使用reshape(1, -1)时,NumPy会根据原始数组的形状和1这个参数,自动计算出合适的列数,使得改变形状后的数组元素个数不变。
三、范数与权重衰减
从上面的公式,我们可以很明显的得到如下结论:
模型的权重越大,Loss就会越大。
λ \lambdaλ 越大,权重衰减的就越厉害
若 λ \lambdaλ 过大,那么原本Loss的占比就会较低,最后模型就光顾着让模型权重变小了,最终模型效果就会变差。
注解:为了防止过拟合 提高泛化性,使用权重衰减的方法,降低模型的复杂度,使模型变得平滑,进而减小过拟合。
它是通过给损失函数增加模型权重L2范数的惩罚(penalty)来让模型权重不要太大,以此来减小模型的复杂度,从而抑制模型的过拟合。 因为上文提过,权重的取值过大也会导致过拟合。我们希望模型将权重均匀分布在每个特征,而不是依赖少数虚假的特征。
L1范数
L1范数是我们经常见到的一种范数,它的定义如下:
表示向量x中非零元素的绝对值之和。
L1范数有很多的名字,例如我们熟悉的 曼哈顿距离、最小绝对误差等。使用 L1范数可以度量两个向量间的差异,如绝对误差和(Sum of Absolute Difference)
由于L1范数的天然性质,对L1优化的解是一个稀疏解, 因此L1范数也被叫做稀疏规则算子。 通过L1可以实现特征的稀疏,去掉一些没有信息的特征,例如在对用户的电影爱好做分类的时候,用户有100个特征,可能只有十几个特征是对分类有用的,大部分特征如身高体重等可能都是无用的,利用L1范数就可以过滤掉。(也就是使得本身越小的数直接消失了)做特征选择,直接删除部分特征。稀疏解-简化模型结构,便于分析特征贡献度,适用于高维数据、特征选择。
如:在lasso回归,在损失函数中加入L1正则项(如 λ∥w∥1)后,参数空间被限制在一个菱形区域内。由于菱形的顶点位于坐标轴上,优化过程中权重向量更容易“触碰”顶点,导致部分维度归零。
L2范数
L2范数是我们最常见最常用的范数了,我们用的最多的度量距离欧氏距离就是一种L2范数,它的定义如下:
表示向量元素的平方和再开平方。衡量一个向量到原点的距离。
像L1范数一样,L2也可以度量两个向量间的差异,如平方差和(Sum of Squared Difference)
L2范数通常会被用来做优化目标函数的正则化项(直接加到loss函数中,逐步优化使得loss min,使得权重均匀分布),防止模型为了迎合训练集而过于复杂造成过拟合的情况,从而提高模型的泛化能力。
L1和L2正则先验分别服从什么分布?
面试中遇到的,L1和L2正则先验分别服从什么分布,L1是拉普拉斯分布,L2是高斯分布。
简洁实现:
DL将权重衰减集成到优化器中
def train_concise(wd):
net = nn.Sequential(nn.Linear(num_inputs, 1)) # 定义模型
for param in net.parameters():
param.data.normal_() # 初始化模型参数
loss = nn.MSELoss(reduction='none')
num_epochs, lr = 100, 0.003
# 偏置参数没有衰减
trainer = torch.optim.SGD([
{"params":net[0].weight,'weight_decay': wd}, # 指定权重衰减
{"params":net[0].bias}], lr=lr) # 偏置参数没有衰减,一般不做
animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
xlim=[5, num_epochs], legend=['train', 'test']) # 绘图
for epoch in range(num_epochs):
for X, y in train_iter:
trainer.zero_grad()
l = loss(net(X), y)
l.mean().backward() #在l上进行反向传播
trainer.step() # 更新参数
if (epoch + 1) % 5 == 0:
animator.add(epoch + 1,
(d2l.evaluate_loss(net, train_iter, loss),
d2l.evaluate_loss(net, test_iter, loss)))
print('w的L2范数:', net[0].weight.norm().item())
四、dropout
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
# 在第一个全连接层之后添加一个dropout层
nn.Dropout(dropout1),
nn.Linear(256, 256),
nn.ReLU(),
# 在第二个全连接层之后添加一个dropout层
nn.Dropout(dropout2),
nn.Linear(256, 10))
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights);