李沐课代码部分——续集
十二、权重衰退之所以我们要利用权重衰退本质上来说就是为了提防这样的情况出现解释一下的话就是——明明能非常“优雅”地只是用较为平滑但变化幅度不会太大的曲线就可以把这7个点全部串起来但倘若我们不对w这个权重进行限制我们的模型很容易【对一道非常简单的题玩出花来】换句话说就是把一些数据学到根本没必要达到的程度。用较为专业的措辞就是——过拟合公式展示如下完整代码展示import torch from d2l import torch as d2l import matplotlib.pyplot as plt # from torch import nn def synthetic_data(w, b, num_examples): X torch.normal(0, 1, (num_examples, len(w))) y torch.matmul(X, w) b y torch.normal(0, 0.01, y.shape) return X, y.reshape((-1, 1)) ---------------- 1. 合成数据 ---------------- n_train, n_test, num_inputs, batch_size 20, 100, 200, 5# 200表示200维 true_w, true_b torch.ones((num_inputs, 1)) * 0.01, 0.05 train_data synthetic_data(true_w, true_b, n_train) test_data synthetic_data(true_w, true_b, n_test) train_iter d2l.load_array(train_data, batch_size) test_iter d2l.load_array(test_data, batch_size, is_trainFalse) ---------------- 2. 初始化参数 ---------------- def init_params(): w torch.normal(0, 1, size(num_inputs, 1), requires_gradTrue) b torch.zeros(1, requires_gradTrue) return [w, b] ---------------- 3. L2 惩罚 ---------------- def l2_penalty(w): return torch.sum(w.pow(2)) / 2 ---------------- 4. 训练函数 ---------------- def train(lambd): w, b init_params() net, loss lambda X: d2l.linreg(X, w, b), d2l.squared_loss num_epochs, lr 100, 0.003 animator d2l.Animator(xlabelepochs, ylabelloss, yscalelog, xlim[5, num_epochs], legend[train, test]) for epoch in range(num_epochs): for X, y in train_iter: # 增加了L2范数惩罚项 # 广播机制使l2_penalty(w)成为一个长度为batch_size的向量 l loss(net(X), y) lambd * l2_penalty(w) l.sum().backward() d2l.sgd([w, b], lr, batch_size) if (epoch 1) % 5 0: with torch.no_grad(): train_loss d2l.evaluate_loss(net, train_iter, loss) test_loss d2l.evaluate_loss(net, test_iter, loss) animator.add(epoch 1, (train_loss, test_loss)) print(w的L2范数是, torch.norm(w).item()) plt.show() ---------------- 5. 运行实验 ---------------- train(lambd0) # 无正则化——过拟合 train(lambd3) # 有权重衰退——泛化更好具体机制解析1. 合成数据中的* 0.01——是对我们生成的w这个200行1列的向量中的每个1通过这个广播机制乘以0.012. l2_penalty函数中的算法是L^2范数或者说是L2范数的计算公式——之所以这样写是因为方便后续求导train函数时直接消掉下面的式子里w前的系数||w|| √(Σwᵢ²)3. train函数的解析1. w, b init_params()是调用我们前面定义的函数用于初始化超参数2. net, loss lambda X: d2l.linreg(X, w, b), d2l.squared_loss这是python的一个语法糖用于临时定义一个函数而不费地方内存让我们只需要一行便可以定义一个急需用的函数。它等价于如下形式——def net(X):return d2l.linreg(X, w, b)loss d2l.squared_loss这行代码大家应该很熟悉虽然看起来有点区别。它本质上就是这样的——def squared_loss(y_hat, y):return (y_hat - y.reshape(y_hat.shape)).pow(2) / 2 # 依旧是除以二以便后续求导梯度下降时消掉2这个系数4. animator d2l.Animator(xlabelepochs, ylabelloss, yscalelog,xlim[5, num_epochs], legend[train, test])xlim [5, num_epochs]的意思是我们只画第5轮之后的答案前面的因为波动太大所以不画5. for epoch in range(num_epochs):for X, y in train_iter:# 增加了L2范数惩罚项# 广播机制使l2_penalty(w)成为一个长度为batch_size的向量l loss(net(X), y) lambd * l2_penalty(w) # 公式的代码形式是整个训练的核心l.sum().backward()d2l.sgd([w, b], lr, batch_size)if (epoch 1) % 5 0:with torch.no_grad():train_loss d2l.evaluate_loss(net, train_iter, loss)test_loss d2l.evaluate_loss(net, test_iter, loss)animator.add(epoch 1, (train_loss, test_loss))print(w的L2范数是, torch.norm(w).item())这部分是真正开始训练的循环with用法表示临时禁止在构建计算图时传梯度换句话说就是提高我们的安全性。然后就是到animator上面我们每跑一组数据就把损失结果画在图上然后打印出来此时的w的L2范数是多少最后通过plt.show()将图画出来十三、丢弃法它是跟权重衰退不一样的提升模型鲁棒性的方法。完整代码展示import torch from torch import nn from d2l import torch as d2l from matplotlib import pyplot as plt 1. 定义丢弃函数 def dropout_layer(X, dropout): assert 0 dropout 1 之所以写assert是因为要提前拦截非法输入 # 在本情况中所有元素都被丢弃 if dropout 1: return torch.zeros_like(X) # 在本情况中所有元素都被保留 if dropout 0: return X mask (torch.Tensor(X.shape).uniform_(0, 1) dropout).float() # mask是一个与X形状相同的张量其值来自伯努利分布 return mask * X / (1.0 - dropout) 2. 初始化数据 num_inputs, num_outputs, num_hidden1, num_hidden2 784, 10, 256, 256 dropout1, dropout2 0.2, 0.5 3. 定义神经网络 class Net(nn.Module): def __init__(self, num_inputs, num_outputs, num_hidden1, num_hidden2, is_training True): super(Net, self).__init__() self.num_inputs num_inputs self.training is_training self.lin1 nn.Linear(num_inputs, num_hidden1) self.lin2 nn.Linear(num_hidden1, num_hidden2) self.lin3 nn.Linear(num_hidden2, num_outputs) self.relu nn.ReLU() def forward(self, X): H1 self.relu(self.lin1(X.reshape((-1, self.num_inputs)))) # 只有在训练模型时才使用丢弃法 if self.training: H1 dropout_layer(H1, dropout1) H2 self.relu(self.lin2(H1)) if self.training: H2 dropout_layer(H2, dropout2) out self.lin3(H2) return out 4. 初始化网络 net Net(num_inputs, num_outputs, num_hidden1, num_hidden2) 5. 训练网络 if __name__ __main__: num_epochs, lr, batch_size 10, 0.5, 256 loss nn.CrossEntropyLoss(reductionnone) train_iter, test_iter d2l.load_data_fashion_mnist(batch_size) trainer torch.optim.SGD(net.parameters(), lrlr) d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer) plt.show()具体机制解析def dropout_layer(X, dropout):assert 0 dropout 1之所以写assert是因为要提前拦截非法输入# 在本情况中所有元素都被丢弃if dropout 1:return torch.zeros_like(X)# 在本情况中所有元素都被保留if dropout 0:return Xmask (torch.Tensor(X.shape).uniform_(0, 1) dropout).float()# mask是一个与X形状相同的张量其值来自伯努利分布return mask * X / (1.0 - dropout)1. assert的用法其实我们从后续紧跟着的不等式可以推断出——是保证dropout这个变量【对应数学里的p这个概率】的值在0到1之间实际上assert的用法也是如此2. mask的用法不是掩码而是一种布尔判断一样通过后续的内容推断出来具体分点来说就是1. 先创建一个跟X形状相同的新张量未初始化的2. .uniform_(0, 1)是对这个我们刚创建的新张量通过原地操作in-place填充[0, 1)区间内注意是左闭右开区间3. dropout的判断大于则为True反之则为False比如dropout 0.4那么0.6的位置为True——每个神经元保留的概率为70%换句话说我们原来就是通过.uniform_(0, 1)来实现“抽奖”创建奖池然后每个神经元【虽然不是它自己抽但程序会替神经元决定去留——通过如上所述的“抽奖”】通过dropout的判断来“抽奖”抽到比dropout大的话这个神经元就保留反之则丢弃。4. .float()的转变毕竟我们整个模型训练都涉及到梯度下降、参数更新……所以还是转成float省事万一有隐性的数据格式兼容问题呢是不是所以还是转成float更省心。def __init__(self, num_inputs, num_outputs, num_hidden1, num_hidden2, is_training True):super(Net, self).__init__()self.num_inputs num_inputsself.training is_trainingself.lin1 nn.Linear(num_inputs, num_hidden1)self.lin2 nn.Linear(num_hidden1, num_hidden2)self.lin3 nn.Linear(num_hidden2, num_outputs)self.relu nn.ReLU()在解释整个类之前我要先说明一点这里的Net类是有继承nn.Module这个父类的这就涉及到super的用法了——super(Net, self)返回一个代理对象代表着它继承的nn.Module父类它后面加的.__init__()这个方法实际上就是继承自nn.Module这个父类所有的方法之后的就是线性层——nn.Linear(num_inputs, num_hidden1)就是在定义一个全连接层格式如下nn.Linear(in_features, out_features)其中的in_features必须匹配上一层的输入out_features是输入到下一层的输出或者是最后的输出如果没有下一层的话最后一行的 self.relu nn.ReLU()就是在定义激活函数用ReLU函数——def forward(self, X):H1 self.relu(self.lin1(X.reshape((-1, self.num_inputs))))# 只有在训练模型时才使用丢弃法if self.training: H1 dropout_layer(H1, dropout1)H2 self.relu(self.lin2(H1))if self.training: H2 dropout_layer(H2, dropout2)out self.lin3(H2)return out这个函数就是在复现这段表达loss nn.CrossEntropyLoss(reductionnone)这就是交叉熵损失的表达形式——cross-Entropy(loss)封装为函数版关于reduction的讲解——reduction值行为返回形状假设 batch_sizeNmean默认对所有样本损失求平均标量[]或torch.Size([])sum对所有样本损失求和标量none不汇总保留每个样本的损失[N]一维张量长度批大小net.parameters()生成一个generator对象是数据生成器直接用于循环、训练中十四、多层感知机、参数初始化1. nn.linear(in_features3, out_features5)一个可学习仿射变换模块不执行计算只负责检查输维度是否匹配真正的计算在.forward(x)发生。完整代码展示import torch from torch import nn from d2l import torch as d2l batch_size 256 train_iter, test_iter d2l.load_data_fashion_mnist(batch_size) #以上是2个数据迭代器 ---------------------1. 初始化模型参数单隐藏层版--------------------- num_inputs, num_outputs, num_hidden 784, 10, 256 W1 nn.Parameter(torch.randn(num_inputs, num_hidden, requires_gradTrue) * 0.01) b1 nn.Parameter(torch.zeros(num_hidden, requires_gradTrue)) W2 nn.Parameter(torch.randn(num_hidden, num_outputs, requires_gradTrue) * 0.01) b2 nn.Parameter(torch.zeros(num_outputs, requires_gradTrue)) params [W1, b1, W2, b2] ---------------------2. 定义激活函数--------------------- def relu(X): a torch.zeros_like(X) return torch.max(X, a) ---------------------3. 定义模型--------------------- def net(X): X X.reshape((-1, num_inputs)) H relu(torch.matmul(X, W1) b1) return torch.matmul(H, W2) b2 ---------------------4. 定义损失函数--------------------- loss nn.CrossEntropyLoss(reductionnone) ---------------------5. 定义优化算法--------------------- updater torch.optim.SGD(params, lr0.1) ---------------------6. 训练模型--------------------- num_epochs, lr 10, 0.1 d2l.train_ch13(net, train_iter, test_iter, loss, num_epochs, updater) d2l.predict_ch3(net, test_iter) ---------------------7. 可视化--------------------- d2l.plt.show()具体机制解析导包、数据迭代器部分就不再赘述了num_inputs, num_outputs, num_hidden 784, 10, 2561. num_inputs输入层的神经元个数为784【28*28784为Fashion-MNIST的尺寸大小】2. num_outputs输出层的神经元个数为10【人为规定的】3. num_hidden隐藏层单层的神经元个数为256【人为规定的】---------------------1. 初始化模型参数单隐藏层版---------------------num_inputs, num_outputs, num_hidden 784, 10, 256W1 nn.Parameter(torch.randn(num_inputs, num_hidden, requires_gradTrue) * 0.01)# 这里之所以*0.01是为了避免数字太大导致梯度爆炸/消失。下面同理b1 nn.Parameter(torch.zeros(num_hidden, requires_gradTrue))#b1是要加到【目标层】上的显然应该是num_hidden。下面同理W2 nn.Parameter(torch.randn(num_hidden, num_outputs, requires_gradTrue) * 0.01)b2 nn.Parameter(torch.zeros(num_outputs, requires_gradTrue))params [W1, b1, W2, b2]#以列表这一数据容器来收集所有可学习参数以供优化器使用但是我想问大家一个问题为什么这里非要搞2组权重、偏置让我们先回到感知机部分理论讲解时我们知道若是只有一组权重、偏置则只能进行多分类问题如softmax回归或者预测趋势如线性回归那是我们非常熟悉的。但实际工程中可不只有如此“线性”的东西比如银行审核“储蓄/借记卡申请”输入是一个excel表格可能高达十几个“特征”但最后是一个二分类问题——批准还是不批准如果不批准理由是什么——如果只有一组权重、偏置那就远远不够了或者给出MLP这样的应用场景手写数字识别 (MNIST)。这是单隐藏层MLP最经典的“Hello World”编程名梗不知道的可以自己去搜一下。因为图片尺寸小28×28、背景还干净单隐藏层比如256个神经元就能达到97%以上的准确率也就是上面我们的例子里讲的而且还有一个原因如果只有一组权重、偏置连【异或问题XOR】都无法解决如下面的异或问题我们使用的是【一组权重、偏置】得出来的情况如下显然不尽人意无法将绿色跟红色完全分开1. 第一组权重、偏置是“提取特征”用的2. 第二组权重、偏置是“类别适配度预测”用的---------------------2. 定义激活函数---------------------def relu(X):a torch.zeros_like(X)#之所以不用torch.zeros(X.shape)是因为上面版本会自动继承X的所有元信息如float16而torch.zeros(X.shape)默认float32后续运算容易出现隐患return torch.max(X, a)zeros_like的解释我觉得若是想通俗易懂些可以这么理解——1. 就好像它zeros_like根据X的元信息“复印”出来一份完全相同的“纸”只是后来它在这张纸上填满了0毕竟是zeros_like而不是别的_like2. 必须跟X维度、尺寸匹配否则没法比较就好像我们不能拿着3*3的矩阵跟2*4的矩阵比较里面的元素一样——尺寸/维度都不一样有可能前面矩阵的某个元素都无法在后面矩阵找到对应---------------------3. 定义模型前向传播---------------------def net(X):X X.reshape((-1, num_inputs))H relu(torch.matmul(X, W1) b1)# 之所以将变量取名为H不是没有原因的return torch.matmul(H, W2) b21. H的含义是“hidden”2. 我们这两行代码H所在的、return所在的计算的数据的解析a) 第一行的数据由输入层流入先经过reshape让输入的数据得以跟“提取特征权重”【合法】进行矩阵乘法不是线性代数里面的那种再将计算结果作为隐藏层的输出同时作为输出层的输入b) 第二行的数据由隐藏层流入与上同理输出是【原始的类别适配度预测】注意此时的数据还未经softmax“概率化”只是作为原始数据---------------------4. 定义损失函数---------------------loss nn.CrossEntropyLoss(reductionnone)模型这里使用的损失函数实际上是【LogSoftmax NLLLOSS】具体解析假设我们的模型输出了3个类别的分数logits[2.0, 1.0, 0.1]我们假设“正确答案”应该是“第一个”也就是数字2.0对应的类别。那么我们的模型在拿到上一个小板块net函数的计算结果后进行的计算过程如下1. Softmax回归公式不再赘述将原始数据“概率化”-[0.659, 0.242, 0.099]2. LogSoftmax取对数将上一步数据取以e为底的自然对数因为本身公式就是以e为底数的指数运算-[-0.417, -1.418, -2.313]3. NLLLOSSNegative Log Likelihood(中文意思是似然)根据真实标签训练时推理时不用给“正确答案”对应的取值“取负号”得到最终的损失值大家可能会有问题我为什么不用之前讲过的Softmax MSE均方误差答最关键的原因是因为“梯度消失”。模型若是非常自信地说是某个答案比如说模型说的这个答案模型觉得对应99%可能但预测错了那么此时Softmax的输出应该无限接近于1 or 0此时Softmax函数的梯度非常趋近于0容易导致梯度消失。这意味着如果模型错得越离谱居然“学得越慢”这很不符合常理也不应该出现这样的情况所以我们不用这样的组合。---------------------5. 定义优化算法优化器---------------------updater torch.optim.SGD(params, lr0.1)讲过很多遍了不再赘述。---------------------6. 训练模型---------------------num_epochs, lr 10, 0.1d2l.train_ch13(net, train_iter, test_iter, loss, num_epochs, updater)d2l.predict_ch3(net, test_iter)调用我们写的函数不再赘述。---------------------7. 可视化---------------------d2l.plt.show()可视化的方法当然不止这一种如果想看图则必须加上类似这样的代码十五、图像卷积完整代码展示import torch from torch import nn #from d2l import torch as d2l def corr2d(x, K): 计算二维互相关/交叉相关运算——卷积的“白盒”实现 h, w K.shape Y torch.zeros( (x.shape[0] - h 1, x.shape[1] - w 1) ) for i in range(Y.shape[0]): for j in range(Y.shape[1]): Y[i, j] ( x[i:i h, j:j w] * K ).sum() #公式是Y (n_h - k_h 1) × (n_w - k_w 1)且Y其实是矩阵 return Y # 以上是手动版本实现卷积实际上在工程中的实现版本如下黑箱变白箱版不是直接调包哦 class Conv2D(nn.Module): def __init__(self, kernel_size): super().__init__()# 子类对父类的复写 self.weight nn.Parameter(torch.rand(kernel_size)) # 随机地根据卷积核尺寸大小初始化参数 Parameter/pəˈræmɪtə(r)/ 中文意思是【可学习参数】。本质上是个张量但并非一开始就固定的参数 self.bias nn.Parameter(torch.zeros(1))# 初始化偏置为1 def forward(self, x): return corr2d(x, self.weight) self.bias # 验证 # X torch.tensor( # [ # [0.0, 1.0, 2.0], # [3.0, 4.0, 5.0], # [6.0, 7.0, 8.0] # ] # ) # K torch.tensor( # [ # [0.0, 1.0], # [2.0, 3.0] # ] # ) # print(corr2d(X, K)) # 接下来模拟检测图像中的不同颜色的边缘 X torch.ones((6, 8)) X[:, 2:6] 0 print(X torch.ones((6, 8)) \n X[:, 2:6] 0) print(X) tensor([[1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.], [1., 1., 0., 0., 0., 0., 1., 1.]]) K torch.tensor([[1.0, -1.0]]) Y corr2d(X, K) # Y中的 1 代表从白色到黑色的边缘 -1 代表从黑色到白色的边缘\ print(K torch.tensor([[1.0, -1.0]]) \n Y corr2d(X, K)) print(Y) tensor([[ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.], [ 0., 1., 0., 0., 0., -1., 0.]]) # 但鸡肋的是——这样的卷积核只能检测竖直边缘如果我们把输入转置 print(corr2d(X.t(), K)) print(corr2d(X.t(), K)) tensor([[0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.]]) 卷积核毫无反应 # 学习由X生成Y的卷积核 conv2d nn.Conv2d(1, 1, kernel_size(1, 2), biasFalse)#暂时不加偏置 X X.reshape((1, 1, 6, 8)) Y Y.reshape((1, 1, 6, 7)) for i in range(10): Y_hat conv2d(X) l (Y_hat - Y) ** 2 conv2d.zero_grad() # 梯度清零 l.sum().backward() # 反向传播 conv2d.weight.data - 3e-2 * conv2d.weight.grad # 梯度下降 if (i 1) % 2 0: print(fepoch {i1}, loss {l.sum():.3f}) print( conv2d.weight.data.reshape( (1, 2) ) )具体机制解析Y torch.zeros( (x.shape[0] - h 1, x.shape[1] - w 1) )for i in range(Y.shape[0]):for j in range(Y.shape[1]):Y[i, j] ( x[i:i h, j:j w] * K ).sum()#公式是Y (n_h - k_h 1) × (n_w - k_w 1)且Y其实是矩阵return Y1. Y torch.zeros( (x.shape[0] - h 1, x.shape[1] - w 1) )我们之所以只选择这个是因为zeros会自动给我们分配好规定的连续存储空间而如果我们用append的话1. 动态分配空间的成本还是挺高的2. PyTorch张量本身不支持如Python列表list那般append我们必须先确定一个形状的张量才能写Y[i, j] ……若y不存在或者形状不对代码都会报错相信很多学者对形状报错都深有体会2. 两个for循环本质上就是等价于我们经常写的两个for循环进行二维矩阵内容输入的“套路”先行然后进入循环——固定在该行逐列输入数据3. Y[i, j] ( x[i:i h, j:j w] * K ).sum()要注意的是——这里Y其实是个矩阵4. return Y返回Y值class Conv2D(nn.Module):def __init__(self, kernel_size):super().__init__()# 子类对父类的复写self.weight nn.Parameter(torch.rand(kernel_size))# 随机地根据卷积核尺寸大小初始化参数 Parameter/pəˈræmɪtə(r)/ 中文意思是【可学习参数】。本质上是个张量但并非一开始就固定的参数self.bias nn.Parameter(torch.zeros(1))# 初始化偏置为1def forward(self, x):return corr2d(x, self.weight) self.bias1. return corr2d(x, self.weight) self.bias等价于我们前面写的conv2d nn.Conv2d(1, 1, kernel_size(1, 2), biasFalse)#暂时不加偏置X X.reshape((1, 1, 6, 8))Y Y.reshape((1, 1, 6, 7))for i in range(10):Y_hat conv2d(X)l (Y_hat - Y) ** 2conv2d.zero_grad() # 梯度清零l.sum().backward() # 反向传播conv2d.weight.data - 3e-2 * conv2d.weight.grad # 梯度下降if (i 1) % 2 0:print(fepoch {i1}, loss {l.sum():.3f})print( conv2d.weight.data.reshape( (1, 2) ) )1.conv2d.weight.data - 3e-2 * conv2d.weight.grad学习率3e-2等价于3e^(-2)这行代码表面上看起来陌生实际上就是我们经常唠叨的W_新 W_旧 - η*只是这里的学习率直接写成了常数而已十六、卷积中的填充与步幅1. 填充为了方便讨论我们此处取s_h1import torch from torch import nn def comp_conv2d(conv2d, X): X X.reshape((1, 1) X.shape) Y conv2d(X) return Y.reshape(Y.shape[2:]) # 填充 conv2d nn.Conv2d(1, 1, 3, padding1) X torch.rand(size(8, 8)) print(comp_conv2d(conv2d, X).shape) # torch.Size([8, 8]) conv2d nn.Conv2d(1, 1, kernel_size(5, 3), padding(2, 1)) comp_conv2d(conv2d, X).shape # torch.Size([8, 8]) 【李沐老师原PPT的讲解我觉得有问题】2. 步幅# 步幅——直接加在填充那部分代码后面就行 conv2d nn.Conv2d(1, 1, kernel_size3, padding1, stride2) print(comp_conv2d(conv2d, X).shape) # torch.Size([4, 4])其实公式实际上的样子应该是这样的注意表示的是向下取整即如果x3.5那么3而不是4我们可以这样想【向下取整】仅列举几个数字4321向下就是方向由4~1反之则是由1~4s_h 1计算证明