本文最后更新于:3 个月前
[√] 第3章 - 线性分类 分类是机器学习中最常见的一类任务,其预测标签是一些离散的类别(符号)。根据分类任务的类别数量又可以分为二分类任务和多分类任务。
线性分类是指利用一个或多个线性函数将样本进行分类。常用的线性分类模型有Logistic回归和Softmax回归。 Logistic回归是一种常用的处理二分类问题的线性模型。Softmax回归是Logistic回归在多分类问题上的推广。
在学习本章内容前,建议您先阅读《神经网络与深度学习》第3章:线性模型的相关内容,关键知识点如 图3.1 所示,以便更好的理解和掌握相应的理论知识,及其在实践中的应用方法。
本章内容基于 《神经网络与深度学习》第3章:线性模型 相关内容进行设计,主要包含两部分:
[√] 3.1 - 基于Logistic回归的二分类任务 在本节中,我们实现一个Logistic回归模型,并对一个简单的数据集进行二分类实验。
[√] 3.1.1 - 数据集构建 我们首先构建一个简单的分类任务,并构建训练集、验证集和测试集。 本任务的数据来自带噪音的两个弯月形状函数,每个弯月对一个类别。我们采集1000条样本,每个样本包含2个特征。
数据集的构建函数make_moons
的代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 import mathimport copyimport paddledef make_moons (n_samples=1000 , shuffle=True , noise=None ): """ 生成带噪音的弯月形状数据 输入: - n_samples:数据量大小,数据类型为int - shuffle:是否打乱数据,数据类型为bool - noise:以多大的程度增加噪声,数据类型为None或float,noise为None时表示不增加噪声 输出: - X:特征数据,shape=[n_samples,2] - y:标签数据, shape=[n_samples] """ n_samples_out = n_samples // 2 n_samples_in = n_samples - n_samples_out outer_circ_x = paddle.cos(paddle.linspace(0 , math.pi, n_samples_out)) outer_circ_y = paddle.sin(paddle.linspace(0 , math.pi, n_samples_out)) inner_circ_x = 1 - paddle.cos(paddle.linspace(0 , math.pi, n_samples_in)) inner_circ_y = 0.5 - paddle.sin(paddle.linspace(0 , math.pi, n_samples_in)) print ('outer_circ_x.shape:' , outer_circ_x.shape, 'outer_circ_y.shape:' , outer_circ_y.shape) print ('inner_circ_x.shape:' , inner_circ_x.shape, 'inner_circ_y.shape:' , inner_circ_y.shape) X = paddle.stack( [paddle.concat([outer_circ_x, inner_circ_x]), paddle.concat([outer_circ_y, inner_circ_y])], axis=1 ) print ('after concat shape:' , paddle.concat([outer_circ_x, inner_circ_x]).shape) print ('X shape:' , X.shape) y = paddle.concat( [paddle.zeros(shape=[n_samples_out]), paddle.ones(shape=[n_samples_in])] ) print ('y shape:' , y.shape) if shuffle: idx = paddle.randperm(X.shape[0 ]) X = X[idx] y = y[idx] if noise is not None : X += paddle.normal(mean=0.0 , std=noise, shape=X.shape) return X, y
随机采集1000个样本,并进行可视化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 n_samples = 1000 X, y = make_moons(n_samples=n_samples, shuffle=True , noise=0.5 ) %matplotlib inlineimport matplotlib.pyplot as plt plt.figure(figsize=(5 ,5 )) plt.scatter(x=X[:, 0 ].tolist(), y=X[:, 1 ].tolist(), marker='*' , c=y.tolist()) plt.xlim(-3 ,4 ) plt.ylim(-3 ,4 ) plt.savefig('linear-dataset-vis.pdf' ) plt.show()
1 2 3 4 5 outer_circ_x.shape: [500 ] outer_circ_y.shape: [500 ] inner_circ_x.shape: [500 ] inner_circ_y.shape: [500 ] after concat shape: [1000 ] X shape: [1000 , 2 ] y shape: [1000 ]
将1000条样本数据拆分成训练集、验证集和测试集,其中训练集640条、验证集160条、测试集200条。代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 num_train = 640 num_dev = 160 num_test = 200 X_train, y_train = X[:num_train], y[:num_train] X_dev, y_dev = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev] X_test, y_test = X[num_train + num_dev:], y[num_train + num_dev:] y_train = y_train.reshape([-1 ,1 ]) y_dev = y_dev.reshape([-1 ,1 ]) y_test = y_test.reshape([-1 ,1 ])
这样,我们就完成了Moon1000数据集的构建。
1 2 print ("X_train shape: " , X_train.shape, "y_train shape: " , y_train.shape)
1 X_train shape: [640 , 2 ] y_train shape: [640 , 1 ]
1 2 3 print (X_train[:5 ])print (y_train[:5 ])
1 2 3 4 5 6 7 8 9 10 11 12 Tensor(shape=[5 , 2 ], dtype=float32, place=CPUPlace, stop_gradient=True , [[-0.83520460 , 0.72044683 ], [-0.41998285 , 0.75988615 ], [-0.19335055 , 0.87097842 ], [ 1.60602939 , -0.35923928 ], [ 0.56276309 , -0.18897334 ]]) Tensor(shape=[5 , 1 ], dtype=float32, place=CPUPlace, stop_gradient=True , [[0. ], [0. ], [0. ], [1. ], [1. ]])
[√] 3.1.2 - 模型构建 Logistic回归是一种常用的处理二分类问题的线性模型。与线性回归一样,Logistic回归也会将输入特征与权重做线性叠加。不同之处在于,Logistic回归引入了非线性函数$g:\mathbb{R}^D \rightarrow (0,1)$,预测类别标签的后验概率 $p(y=1|\mathbf x)$ ,从而解决连续的线性函数不适合进行分类的问题。
$$ p(y=1|\mathbf x) = \sigma(\mathbf w^ \mathrm{ T } \mathbf x+b),(3.1) $$
其中判别函数$\sigma(\cdot)$为Logistic函数,也称为激活函数,作用是将线性函数$f(\mathbf x;\mathbf w,b)$的输出从实数区间“挤压”到(0,1)之间,用来表示概率。Logistic函数定义为:
$$ \sigma(x) = \frac{1}{1+\exp(-x)}。(3.2) $$
Logistic函数
Logistic函数的代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def logistic (x ): return 1 / (1 + paddle.exp(-x)) x = paddle.linspace(-10 , 10 , 10000 ) plt.figure() plt.plot(x.tolist(), logistic(x).tolist(), color="#E20079" , label="Logistic Function" ) ax = plt.gca() ax.spines['top' ].set_color('none' ) ax.spines['right' ].set_color('none' ) ax.xaxis.set_ticks_position('bottom' ) ax.yaxis.set_ticks_position('left' ) ax.spines['left' ].set_position(('data' ,0 )) ax.spines['bottom' ].set_position(('data' ,0 )) plt.legend() plt.savefig('linear-logistic.pdf' ) plt.show()
从输出结果看,当输入在0附近时,Logistic函数近似为线性函数;而当输入值非常大或非常小时,函数会对输入进行抑制。输入越小,则越接近0;输入越大,则越接近1。正因为Logistic函数具有这样的性质,使得其输出可以直接看作为概率分布。
Logistic回归算子
==Logistic回归模型其实就是线性层与Logistic函数的组合==
通常会将 Logistic回归模型中的权重和偏置初始化为0,同时,为了提高预测样本的效率,我们将$N$个样本归为一组进行成批地预测。 $$ \hat{\mathbf y} = p(\mathbf y|\mathbf x) = \sigma(\boldsymbol{X} \boldsymbol{w} + b), (3.3) $$
其中$\boldsymbol{X}\in \mathbb{R}^{N\times D}$为$N$个样本的特征矩阵,$\hat{\boldsymbol{y}}$为$N$个样本的预测值构成的$N$维向量。
这里,我们构建一个Logistic回归算子,代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from nndl import opclass model_LR (op.Op): def __init__ (self, input_dim ): super (model_LR, self).__init__() self.params = {} self.params['w' ] = paddle.zeros(shape=[input_dim, 1 ]) self.params['b' ] = paddle.zeros(shape=[1 ]) def __call__ (self, inputs ): return self.forward(inputs) def forward (self, inputs ): """ 输入: - inputs: shape=[N,D], N是样本数量,D为特征维度 输出: - outputs:预测标签为1的概率,shape=[N,1] """ score = paddle.matmul(inputs, self.params['w' ]) + self.params['b' ] outputs = logistic(score) return outputs
测试一下
随机生成3条长度为4的数据输入Logistic回归模型,观察输出结果。
1 2 3 4 5 6 7 8 9 paddle.seed(0 ) inputs = paddle.randn(shape=[3 ,4 ])print ('Input is:' , inputs) model = model_LR(4 ) outputs = model(inputs)print ('Output is:' , outputs)
1 2 3 4 5 6 7 8 Input is : Tensor(shape=[3 , 4 ], dtype=float32, place=CPUPlace, stop_gradient=True , [[-0.75711036 , -0.38059190 , 0.10946669 , 1.34467661 ], [-0.84002435 , -1.27341712 , 2.47224617 , 0.14070207 ], [ 0.60608417 , 0.23396523 , 1.35604191 , 0.10350471 ]]) Output is : Tensor(shape=[3 , 1 ], dtype=float32, place=CPUPlace, stop_gradient=True , [[0.50000000 ], [0.50000000 ], [0.50000000 ]])
从输出结果看,模型最终的输出$g(·)$恒为0.5。这是由于采用全0初始化后,不论输入值的大小为多少,Logistic函数的输入值恒为0,因此输出恒为0.5。
[√] 3.1.3 - 损失函数 ==线性二分类logistic,通常使用交叉熵作为损失函数。==
在模型训练过程中,需要使用损失函数来量化预测值和真实值之间的差异。 给定一个分类任务,$\mathbf y$表示样本$\mathbf x$的标签的真实概率分布,向量$\hat{\mathbf y}=p(\mathbf y|\mathbf x)$表示预测的标签概率分布。 训练目标是使得$\hat{\mathbf y}$尽可能地接近$\mathbf y$,通常可以使用交叉熵损失函数 。 在给定$\mathbf y$的情况下,如果预测的概率分布$\hat{\mathbf y}$与标签真实的分布$\mathbf y$越接近,则交叉熵越小;如果$p(\mathbf x)$和$\mathbf y$越远,交叉熵就越大。
对于二分类任务,我们只需要计算$\hat{y}=p(y=1|\mathbf x)$,用$1-\hat{y}$来表示$p(y=0|\mathbf x)$。 给定有$N$个训练样本的训练集${(\mathbf x^{(n)},y^{(n)})} ^N_{n=1}$,使用交叉熵损失函数,Logistic回归的风险函数计算方式为:
$$ \begin{aligned} \cal R(\mathbf w,b) &= -\frac{1}{N}\sum_{n=1}^N \Big(y^{(n)}\log\hat{y}^{(n)} + (1-y^{(n)})\log(1-\hat{y}^{(n)})\Big)。(3.4) \end{aligned} $$
向量形式可以表示为:
$$ \begin{aligned} \cal R(\mathbf w,b) &= -\frac{1}{N}\Big(\mathbf y^ \mathrm{ T } \log\hat{\mathbf y} + (1-\mathbf y)^ \mathrm{ T } \log(1-\hat{\mathbf y})\Big),(3.5) \end{aligned} $$
其中$\mathbf y\in [0,1]^N$为$N$个样本的真实标签构成的$N$维向量,$\hat{\mathbf y}$为$N$个样本标签为1的后验概率构成的$N$维向量。
二分类任务的交叉熵损失函数的代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class BinaryCrossEntropyLoss (op.Op): def __init__ (self ): self.predicts = None self.labels = None self.num = None def __call__ (self, predicts, labels ): return self.forward(predicts, labels) def forward (self, predicts, labels ): """ 输入: - predicts:预测值,shape=[N, 1],N为样本数量 - labels:真实标签,shape=[N, 1] 输出: - 损失值:shape=[1] """ self.predicts = predicts self.labels = labels self.num = self.predicts.shape[0 ] loss = -1. / self.num * (paddle.matmul(self.labels.t(), paddle.log(self.predicts)) + paddle.matmul((1 -self.labels.t()), paddle.log(1 -self.predicts))) loss = paddle.squeeze(loss, axis=1 ) return loss labels = paddle.ones(shape=[3 ,1 ]) bce_loss = BinaryCrossEntropyLoss()print (bce_loss(outputs, labels))
1 2 Tensor(shape=[1 ], dtype=float32, place=CPUPlace, stop_gradient=True , [0.69314718 ])
[√] 3.1.4 - 模型优化 不同于线性回归中直接使用最小二乘法即可进行模型参数的求解,Logistic回归需要使用优化算法对模型参数进行有限次地迭代来获取更优的模型,从而尽可能地降低风险函数的值。 在机器学习任务中,最简单、常用的优化算法是梯度下降法。
使用梯度下降法进行模型优化,首先需要初始化参数$\mathbf W$和 $b$,然后不断地计算它们的梯度,并沿梯度的反方向更新参数。
[√] 3.1.4.1 - 梯度计算 在Logistic回归中,风险函数$\cal R(\mathbf w,b)$ 关于参数$\mathbf w$和$b$的偏导数为:
$$ \begin{aligned} \frac{\partial \cal R(\mathbf w,b)}{\partial \mathbf w} = -\frac{1}{N}\sum_{n=1}^N \mathbf x^{(n)}(y^{(n)}- \hat{y}^{(n)}) = -\frac{1}{N} \mathbf X^ \mathrm{ T } (\mathbf y-\hat{\mathbf y}) \end{aligned},(3.6) $$
$$ \begin{aligned} \frac{\partial \cal R(\mathbf w,b)}{\partial b} = -\frac{1}{N}\sum_{n=1}^N (y^{(n)}- \hat{y}^{(n)}) = -\frac{1}{N} \mathbf {sum}(\mathbf y-\hat{\mathbf y}) \end{aligned}。(3.7) $$
通常将偏导数的计算过程定义在Logistic回归算子的backward
函数中,代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class model_LR (op.Op): def __init__ (self, input_dim ): super (model_LR, self).__init__() self.params = {} self.params['w' ] = paddle.zeros(shape=[input_dim, 1 ]) self.params['b' ] = paddle.zeros(shape=[1 ]) self.grads = {} self.X = None self.outputs = None def __call__ (self, inputs ): return self.forward(inputs) def forward (self, inputs ): self.X = inputs score = paddle.matmul(inputs, self.params['w' ]) + self.params['b' ] self.outputs = logistic(score) return self.outputs def backward (self, labels ): """ 输入: - labels:真实标签,shape=[N, 1] """ N = labels.shape[0 ] self.grads['w' ] = -1 / N * paddle.matmul(self.X.t(), (labels - self.outputs)) self.grads['b' ] = -1 / N * paddle.sum (labels - self.outputs)
[√] 3.1.4.2 - 参数更新 在计算参数的梯度之后,我们按照下面公式更新参数:
$$ \mathbf w\leftarrow \mathbf w - \alpha \frac{\partial \cal R(\mathbf w,b)}{\partial \mathbf w},(3.8) $$ $$ b\leftarrow b - \alpha \frac{\partial \cal R(\mathbf w,b)}{\partial b},(3.9) $$
其中$\alpha$ 为学习率。
将上面的参数更新过程包装为优化器,首先定义一个优化器基类Optimizer
,方便后续所有的优化器调用。在这个基类中,需要初始化优化器的初始学习率init_lr
,以及指定优化器需要优化的参数。代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from abc import abstractmethodclass Optimizer (object ): def __init__ (self, init_lr, model ): """ 优化器类初始化 """ self.init_lr = init_lr self.model = model @abstractmethod def step (self ): """ 定义每次迭代如何更新参数 """ pass
然后实现一个梯度下降法的优化器函数SimpleBatchGD
来执行参数更新过程。其中step
函数从模型的grads
属性取出参数的梯度并更新。代码实现如下:
1 2 3 4 5 6 7 8 9 10 class SimpleBatchGD (Optimizer ): def __init__ (self, init_lr, model ): super (SimpleBatchGD, self).__init__(init_lr=init_lr, model=model) def step (self ): if isinstance (self.model.params, dict ): for key in self.model.params.keys(): self.model.params[key] = self.model.params[key] - self.init_lr * self.model.grads[key]
[√] 3.1.5 - 评价指标 在分类任务中,通常使用准确率(Accuracy)作为评价指标。如果模型预测的类别与真实类别一致,则说明模型预测正确。准确率即正确预测的数量与总的预测数量的比值:
$$ \mathcal{A} \frac{1}{N} \sum_{n=1}^N I (y^{(n)} = \hat{y}^{(n)}),(3.10) $$
其中$I(·)$是指示函数。代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def accuracy (preds, labels ): """ 输入: - preds:预测值,二分类时,shape=[N, 1],N为样本数量,多分类时,shape=[N, C],C为类别数量 - labels:真实标签,shape=[N, 1] 输出: - 准确率:shape=[1] """ if preds.shape[1 ] == 1 : preds = paddle.cast((preds>=0.5 ),dtype='float32' ) else : preds = paddle.argmax(preds,axis=1 , dtype='int32' ) return paddle.mean(paddle.cast(paddle.equal(preds, labels),dtype='float32' )) preds = paddle.to_tensor([[0. ],[1. ],[1. ],[0. ]]) labels = paddle.to_tensor([[1. ],[1. ],[0. ],[0. ]])print ("accuracy is:" , accuracy(preds, labels))
1 2 accuracy is : Tensor(shape=[1 ], dtype=float32, place=CPUPlace, stop_gradient=True , [0.50000000 ])
[√] 3.1.6 - 完善Runner类 基于RunnerV1,本章的RunnerV2类在训练过程中使用梯度下降法进行网络优化,模型训练过程中计算在训练集和验证集上的损失及评估指标并打印,训练过程中保存最优模型。代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 class RunnerV2 (object ): def __init__ (self, model, optimizer, metric, loss_fn ): self.model = model self.optimizer = optimizer self.loss_fn = loss_fn self.metric = metric self.train_scores = [] self.dev_scores = [] self.train_loss = [] self.dev_loss = [] def train (self, train_set, dev_set, **kwargs ): num_epochs = kwargs.get("num_epochs" , 0 ) log_epochs = kwargs.get("log_epochs" , 100 ) save_path = kwargs.get("save_path" , "best_model.pdparams" ) print_grads = kwargs.get("print_grads" , None ) best_score = 0 for epoch in range (num_epochs): X, y = train_set logits = self.model(X) trn_loss = self.loss_fn(logits, y).item() self.train_loss.append(trn_loss) trn_score = self.metric(logits, y).item() self.train_scores.append(trn_score) self.model.backward(y) if print_grads is not None : print_grads(self.model) self.optimizer.step() dev_score, dev_loss = self.evaluate(dev_set) if dev_score > best_score: self.save_model(save_path) print (f"best accuracy performence has been updated: {best_score:.5 f} --> {dev_score:.5 f} " ) best_score = dev_score if epoch % log_epochs == 0 : print (f"[Train] epoch: {epoch} , loss: {trn_loss} , score: {trn_score} " ) print (f"[Dev] epoch: {epoch} , loss: {dev_loss} , score: {dev_score} " ) def evaluate (self, data_set ): X, y = data_set logits = self.model(X) loss = self.loss_fn(logits, y).item() self.dev_loss.append(loss) score = self.metric(logits, y).item() self.dev_scores.append(score) return score, loss def predict (self, X ): return self.model(X) def save_model (self, save_path ): paddle.save(self.model.params, save_path) def load_model (self, model_path ): self.model.params = paddle.load(model_path)
[√] 3.1.7 - 模型训练 下面进行Logistic回归模型的训练,使用交叉熵损失函数和梯度下降法进行优化。 使用训练集和验证集进行模型训练,共训练 500个epoch,每隔50个epoch打印出训练集上的指标。 代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 paddle.seed(102 ) input_dim = 2 lr = 0.1 model = model_LR(input_dim=input_dim) optimizer = SimpleBatchGD(init_lr=lr, model=model) loss_fn = BinaryCrossEntropyLoss() metric = accuracy runner = RunnerV2(model, optimizer, metric, loss_fn) runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=500 , log_epochs=50 , save_path="best_model.pdparams" )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 best accuracy performence has been updated: 0.00000 --> 0.81250 [Train] epoch: 0 , loss: 0.6931471824645996 , score: 0.5140625238418579 [Dev] epoch: 0 , loss: 0.681858479976654 , score: 0.8125 best accuracy performence has been updated: 0.81250 --> 0.81875 best accuracy performence has been updated: 0.81875 --> 0.82500 [Train] epoch: 300 , loss: 0.4530242085456848 , score: 0.7875000238418579 [Dev] epoch: 300 , loss: 0.374613493680954 , score: 0.824999988079071 best accuracy performence has been updated: 0.82500 --> 0.83125 [Train] epoch: 600 , loss: 0.45098423957824707 , score: 0.7875000238418579 [Dev] epoch: 600 , loss: 0.36696094274520874 , score: 0.831250011920929 [Train] epoch: 900 , loss: 0.4508608877658844 , score: 0.7906249761581421 [Dev] epoch: 900 , loss: 0.3654332160949707 , score: 0.831250011920929 [Train] epoch: 1200 , loss: 0.4508519172668457 , score: 0.7906249761581421 [Dev] epoch: 1200 , loss: 0.3650430142879486 , score: 0.831250011920929 [Train] epoch: 1500 , loss: 0.4508512616157532 , score: 0.7906249761581421 [Dev] epoch: 1500 , loss: 0.36493727564811707 , score: 0.831250011920929 [Train] epoch: 1800 , loss: 0.450851172208786 , score: 0.7906249761581421 [Dev] epoch: 1800 , loss: 0.3649081885814667 , score: 0.831250011920929 [Train] epoch: 2100 , loss: 0.4508512020111084 , score: 0.7906249761581421 [Dev] epoch: 2100 , loss: 0.3649001121520996 , score: 0.831250011920929 [Train] epoch: 2400 , loss: 0.4508512020111084 , score: 0.7906249761581421 [Dev] epoch: 2400 , loss: 0.3648979365825653 , score: 0.831250011920929 [Train] epoch: 2700 , loss: 0.4508512020111084 , score: 0.7906249761581421 [Dev] epoch: 2700 , loss: 0.36489737033843994 , score: 0.831250011920929
可视化观察训练集与验证集的准确率和损失的变化情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 def plot (runner,fig_name ): plt.figure(figsize=(10 ,5 )) plt.subplot(1 ,2 ,1 ) epochs = [i for i in range (len (runner.train_scores))] plt.plot(epochs, runner.train_loss, color='#8E004D' , label="Train loss" ) plt.plot(epochs, runner.dev_loss, color='#E20079' , linestyle='--' , label="Dev loss" ) plt.ylabel("loss" ) plt.xlabel("epoch" ) plt.legend(loc='upper right' ) plt.subplot(1 ,2 ,2 ) plt.plot(epochs, runner.train_scores, color='#8E004D' , label="Train accuracy" ) plt.plot(epochs, runner.dev_scores, color='#E20079' , linestyle='--' , label="Dev accuracy" ) plt.ylabel("score" ) plt.xlabel("epoch" ) plt.legend(loc='lower right' ) plt.tight_layout() plt.savefig(fig_name) plt.show() plot(runner,fig_name='linear-acc.pdf' )
从输出结果可以看到,在训练集与验证集上,loss得到了收敛,同时准确率指标都达到了较高的水平,训练比较充分。
[√] 3.1.8 - 模型评价 使用测试集对训练完成后的最终模型进行评价,观察模型在测试集上的准确率和loss数据。代码实现如下:
1 2 score, loss = runner.evaluate([X_test, y_test])print ("[Test] score/loss: {:.4f}/{:.4f}" .format (score, loss))
1 [Test] score/loss: 0.7750 /0.4776
可视化观察拟合的决策边界 $\boldsymbol{X} \boldsymbol{w} + b=0$。
1 2 3 4 def decision_boundary (w, b, x1 ): w1, w2 = w x2 = (- w1 * x1 - b) / w2 return x2
1 2 3 4 5 6 7 8 9 10 11 plt.figure(figsize=(5 ,5 )) plt.scatter(X[:, 0 ].tolist(), X[:, 1 ].tolist(), marker='*' , c=y.tolist()) w = model.params['w' ] b = model.params['b' ] x1 = paddle.linspace(-2 , 3 , 1000 ) x2 = decision_boundary(w, b, x1) plt.plot(x1.tolist(), x2.tolist(), color="red" ) plt.show()
[√] 3.2 - 基于Softmax回归的多分类任务 Logistic回归可以有效地解决二分类问题,但在分类任务中,还有一类多分类问题,即类别数$C$大于2 的分类问题。Softmax回归就是Logistic回归在多分类问题上的推广。
使用Softmax回归模型对一个简单的数据集进行多分类实验。
[√] 3.2.1 - 数据集构建 我们首先构建一个简单的多分类任务,并构建训练集、验证集和测试集。 本任务的数据来自3个不同的簇,每个簇对一个类别。我们采集1000条样本,每个样本包含2个特征。
数据集的构建函数make_multi
的代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import numpy as npdef make_multiclass_classification (n_samples=100 , n_features=2 , n_classes=3 , shuffle=True , noise=0.1 ): """ 生成带噪音的多类别数据 输入: - n_samples:数据量大小,数据类型为int - n_features:特征数量,数据类型为int - shuffle:是否打乱数据,数据类型为bool - noise:以多大的程度增加噪声,数据类型为None或float,noise为None时表示不增加噪声 输出: - X:特征数据,shape=[n_samples,2] - y:标签数据, shape=[n_samples,1] """ n_samples_per_class = [int (n_samples / n_classes) for k in range (n_classes)] for i in range (n_samples - sum (n_samples_per_class)): n_samples_per_class[i % n_classes] += 1 X = paddle.zeros([n_samples, n_features]) y = paddle.zeros([n_samples], dtype='int32' ) centroids = paddle.randperm(2 ** n_features)[:n_classes] centroids_bin = np.unpackbits(centroids.numpy().astype('uint8' )).reshape((-1 , 8 ))[:, -n_features:] centroids = paddle.to_tensor(centroids_bin, dtype='float32' ) centroids = 1.5 * centroids - 1 X[:, :n_features] = paddle.randn(shape=[n_samples, n_features]) stop = 0 for k, centroid in enumerate (centroids): start, stop = stop, stop + n_samples_per_class[k] y[start:stop] = k % n_classes X_k = X[start:stop, :n_features] A = 2 * paddle.rand(shape=[n_features, n_features]) - 1 X_k[...] = paddle.matmul(X_k, A) X_k += centroid X[start:stop, :n_features] = X_k if noise > 0.0 : noise_mask = paddle.rand([n_samples]) < noise for i in range (len (noise_mask)): if noise_mask[i]: y[i] = paddle.randint(n_classes, shape=[1 ]).astype('int32' ) if shuffle: idx = paddle.randperm(X.shape[0 ]) X = X[idx] y = y[idx] return X, y
随机采集1000个样本,并进行可视化。
1 2 3 4 5 6 7 8 9 10 11 paddle.seed(102 ) n_samples = 1000 X, y = make_multiclass_classification(n_samples=n_samples, n_features=2 , n_classes=3 , noise=0.2 ) plt.figure(figsize=(5 ,5 )) plt.scatter(x=X[:, 0 ].tolist(), y=X[:, 1 ].tolist(), marker='*' , c=y.tolist()) plt.savefig('linear-dataset-vis2.pdf' ) plt.show()
将实验数据拆分成训练集、验证集和测试集。其中训练集640条、验证集160条、测试集200条。
1 2 3 4 5 6 7 8 9 10 num_train = 640 num_dev = 160 num_test = 200 X_train, y_train = X[:num_train], y[:num_train] X_dev, y_dev = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev] X_test, y_test = X[num_train + num_dev:], y[num_train + num_dev:]print ("X_train shape: " , X_train.shape, "y_train shape: " , y_train.shape)
1 X_train shape: [640 , 2 ] y_train shape: [640 ]
这样,我们就完成了Multi1000数据集的构建。
1 2 Tensor(shape=[5 ], dtype=int32, place=CPUPlace, stop_gradient=True , [0 , 1 , 2 , 2 , 0 ])
[√] 3.2.2 - 模型构建 在Softmax回归中,对类别进行预测的方式是预测输入属于每个类别的条件概率。与Logistic 回归不同的是,Softmax回归的输出值个数等于类别数$C$,而每个类别的概率值则通过Softmax函数进行求解。
[√] 3.2.2.1 Softmax函数 Softmax函数可以将多个标量映射为一个概率分布。对于一个$K$维向量,$\mathbf x=[x_1,\cdots,x_K]$,Softmax的计算公式为
$$ \mathrm{softmax}(x_k) = \frac{\exp(x_k)}{\sum_{i=1}^K \exp(x_i)}。(3.11) $$
在Softmax函数的计算过程中,要注意上溢出 和下溢出 的问题。假设Softmax 函数中所有的$x_k$都是相同大小的数值$a$,理论上,所有的输出都应该为$\frac{1}{k}$。但需要考虑如下两种特殊情况:
$a$为一个非常大的负数,此时$\exp(a)$ 会发生下溢出现象。计算机在进行数值计算时,当数值过小,会被四舍五入为0。此时,Softmax函数的分母会变为0,导致计算出现问题;
$a$为一个非常大的正数,此时会导致$\exp(a)$发生上溢出现象,导致计算出现问题。
==通过将softmax公式中的x替换为x-max(x)来避免上溢出和下溢出问题:==
为了解决上溢出和下溢出的问题,在计算Softmax函数时,可以使用$x_k - \max(\mathbf x)$代替$x_k$。 此时,通过减去最大值,$x_k$最大为0,避免了上溢出的问题;同时,分母中至少会包含一个值为1的项,从而也避免了下溢出的问题。
Softmax函数的代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def softmax (X ): """ 输入: - X:shape=[N, C],N为向量数量,C为向量维度 """ x_max = paddle.max (X, axis=1 , keepdim=True ) x_exp = paddle.exp(X - x_max) partition = paddle.sum (x_exp, axis=1 , keepdim=True ) return x_exp / partition X = paddle.to_tensor([[0.1 , 0.2 , 0.3 , 0.4 ],[1 ,2 ,3 ,4 ]]) predict = softmax(X)print (predict)
1 2 3 Tensor(shape=[2 , 4 ], dtype=float32, place=CPUPlace, stop_gradient=True , [[0.21383820 , 0.23632778 , 0.26118258 , 0.28865141 ], [0.03205860 , 0.08714432 , 0.23688281 , 0.64391422 ]])
对于每个c维输入x,预测输出得到一个总和为1的c维概率分布。
[√] 3.2.2.2 Softmax回归算子 在Softmax回归中,类别标签$y\in{1,2,…,C}$。给定一个样本$\mathbf x$,使用Softmax回归预测的属于类别$c$的条件概率为
$$ \begin{aligned} p(y=c|\mathbf x) &= \mathrm{softmax}(\mathbf w_c^T \mathbf x+b_c),(3.12) \end{aligned} $$
其中$\mathbf w_c$是第 $c$ 类的权重向量,$b_c$是第 $c$ 类的偏置。
Softmax回归模型其实就是线性函数与Softmax函数的组合。
将$N$个样本归为一组进行成批地预测。
$$ \hat{\mathbf Y} = \mathrm{softmax}(\boldsymbol{X} \boldsymbol{W} + \mathbf b), (3.13) $$
其中$\boldsymbol{X}\in \mathbb{R}^{N\times D}$为$N$个样本的特征矩阵,$\boldsymbol{W}=[\mathbf w_1,……,\mathbf w_C]$为$C$个类的权重向量组成的矩阵,$\hat{\mathbf Y}\in \mathbb{R}^{C}$为所有类别的预测条件概率组成的矩阵。
我们根据公式(3.13)实现Softmax回归算子,代码实现如下:
每个类别有一个权重,因此c个类别就有c组权重,对于一个输入x,x和c组权重相运算,得到c个概率,最大的那个概率对应的类别就是预测的类别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class model_SR (op.Op): def __init__ (self, input_dim, output_dim ): super (model_SR, self).__init__() self.params = {} self.params['W' ] = paddle.zeros(shape=[input_dim, output_dim]) self.params['b' ] = paddle.zeros(shape=[output_dim]) self.outputs = None def __call__ (self, inputs ): return self.forward(inputs) def forward (self, inputs ): """ 输入: - inputs: shape=[N,D], N是样本数量,D是特征维度 输出: - outputs:预测值,shape=[N,C],C是类别数 """ score = paddle.matmul(inputs, self.params['W' ]) + self.params['b' ] self.outputs = softmax(score) return self.outputs inputs = paddle.randn(shape=[1 ,4 ]) print ('Input is:' , inputs) model = model_SR(input_dim=4 , output_dim=3 ) outputs = model(inputs)print ('Output is:' , outputs)
1 2 3 4 Input is : Tensor(shape=[1 , 4 ], dtype=float32, place=CPUPlace, stop_gradient=True , [[-0.06042910 , 0.97415614 , 0.28900006 , 0.37233669 ]]) Output is : Tensor(shape=[1 , 3 ], dtype=float32, place=CPUPlace, stop_gradient=True , [[0.33333334 , 0.33333334 , 0.33333334 ]])
从输出结果可以看出,采用全0初始化后,属于每个类别的条件概率均为$\frac{1}{C}$。这是因为,不论输入值的大小为多少,线性函数$f(\mathbf x;\mathbf W,\mathbf b)$的输出值恒为0。此时,再经过Softmax函数的处理,每个类别的条件概率恒等。
[√] 3.2.3 - 损失函数 ==Softmax回归同样使用交叉熵损失作为损失函数,并使用梯度下降法对参数进行优化。==
通常使用$C$维的one-hot类型向量$\mathbf y \in {0,1}^C$来表示多分类任务中的类别标签。对于类别$c$,其向量表示为:
$$ \mathbf y = [I(1=c),I(2=c),…,I(C=c)]^T,(3.14) $$
其中$I(·)$是指示函数,即括号内的输入为“真”,$I(·)=1$;否则,$I(·)=0$。
给定有$N$个训练样本的训练集${(\mathbf x^{(n)},y^{(n)})} ^N_{n=1}$,令$\hat{\mathbf y}^{(n)}=\mathrm{softmax}(\mathbf W^ \mathrm{ T } \mathbf x^{(n)}+\mathbf b)$为样本$\mathbf x^{(n)}$在每个类别的后验概率。多分类问题的交叉熵损失函数定义为:
$$ \cal R(\mathbf W,\mathbf b) = -\frac{1}{N}\sum_{n=1}^N (\mathbf y^{(n)})^ \mathrm{ T } \log\hat{\mathbf y}^{(n)} = -\frac{1}{N}\sum_{n=1}^N \sum_{c=1}^C \mathbf y_c^{(n)} \log\hat{\mathbf y}_c^{(n)}.(3.15) $$
观察上式,$\mathbf y_c^{(n)}$在$c$为真实类别时为1,其余都为0。也就是说,交叉熵损失只关心正确类别的预测概率,因此,上式又可以优化为:
$$ \cal R(\mathbf W,\mathbf b) = -\frac{1}{N}\sum_{n=1}^N \log [\hat{\mathbf y}^{(n)}]_{y^{(n)}},(3.16) $$
其中$y^{(n)}$是第$n$个样本的标签。
因此,多类交叉熵损失函数的代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class MultiCrossEntropyLoss (op.Op): def __init__ (self ): self.predicts = None self.labels = None self.num = None def __call__ (self, predicts, labels ): return self.forward(predicts, labels) def forward (self, predicts, labels ): """ 输入: - predicts:预测值,shape=[N, 1],N为样本数量 - labels:真实标签,shape=[N, 1] 输出: - 损失值:shape=[1] """ self.predicts = predicts self.labels = labels self.num = self.predicts.shape[0 ] loss = 0 for i in range (0 , self.num): index = self.labels[i] loss -= paddle.log(self.predicts[i][index]) return loss / self.num labels = paddle.to_tensor([0 ]) mce_loss = MultiCrossEntropyLoss()print (mce_loss(outputs, labels))
1 2 Tensor(shape=[1 ], dtype=float32, place=CPUPlace, stop_gradient=True , [1.09861231 ])
[√] 3.2.4 - 模型优化 使用梯度下降算法进行参数学习
[√] 3.2.4.1 - 梯度计算 计算风险函数$\cal R(\mathbf W,\mathbf b)$关于参数$\mathbf W$和$\mathbf b$的偏导数。在Softmax回归中,计算方法为:
$$ \frac{\partial \cal R(\mathbf W,\mathbf b)}{\partial \mathbf W} = -\frac{1}{N}\sum_{n=1}^N \mathbf x^{(n)}(y^{(n)}- \hat{ y}^{(n)})^T = -\frac{1}{N} \mathbf X^ \mathrm{ T } (\mathbf y- \hat{\mathbf y}),(3.17) $$
$$ \frac{\partial \cal R(\mathbf W,\mathbf b)}{\partial \mathbf b} = -\frac{1}{N}\sum_{n=1}^N (y^{(n)}- \hat{y}^{(n)})^T = -\frac{1}{N} \mathbf 1 (\mathbf y- \hat{\mathbf y}).(3.18) $$
其中$\mathbf X\in \mathbb{R}^{N\times D}$为$N$个样本组成的矩阵,$\mathbf y\in \mathbb{R}^{N}$为$N$个样本标签组成的向量,$\hat{\mathbf y}\in \mathbb{R}^{N}$为$N$个样本的预测标签组成的向量,$\mathbf{1}$为$N$维的全1向量。
将上述计算方法定义在模型的backward
函数中,代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class model_SR (op.Op): def __init__ (self, input_dim, output_dim ): super (model_SR, self).__init__() self.params = {} self.params['W' ] = paddle.zeros(shape=[input_dim, output_dim]) self.params['b' ] = paddle.zeros(shape=[output_dim]) self.grads = {} self.X = None self.outputs = None self.output_dim = output_dim def __call__ (self, inputs ): return self.forward(inputs) def forward (self, inputs ): self.X = inputs score = paddle.matmul(self.X, self.params['W' ]) + self.params['b' ] self.outputs = softmax(score) return self.outputs def backward (self, labels ): """ 输入: - labels:真实标签,shape=[N, 1],其中N为样本数量 """ N =labels.shape[0 ] labels = paddle.nn.functional.one_hot(labels, self.output_dim) self.grads['W' ] = -1 / N * paddle.matmul(self.X.t(), (labels-self.outputs)) self.grads['b' ] = -1 / N * paddle.matmul(paddle.ones(shape=[N]), (labels-self.outputs))
[√] 3.2.4.2 - 参数更新 在计算参数的梯度之后,我们使用3.1.4.2中实现的梯度下降法进行参数更新。
[√] 3.2.5 - 模型训练 实例化RunnerV2类,并传入训练配置。使用训练集和验证集进行模型训练,共训练500个epoch。每隔50个epoch打印训练集上的指标。代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 paddle.seed(102 ) input_dim = 2 output_dim = 3 lr = 0.1 model = model_SR(input_dim=input_dim, output_dim=output_dim) optimizer = SimpleBatchGD(init_lr=lr, model=model) loss_fn = MultiCrossEntropyLoss() metric = accuracy runner = RunnerV2(model, optimizer, metric, loss_fn) runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=500 , log_eopchs=50 , eval_epochs=1 , save_path="best_model.pdparams" ) plot(runner,fig_name='linear-acc2.pdf' )
[√] 3.2.6 - 模型评价 使用测试集对训练完成后的最终模型进行评价,观察模型在测试集上的准确率。代码实现如下:
1 2 score, loss = runner.evaluate([X_test, y_test])print ("[Test] score/loss: {:.4f}/{:.4f}" .format (score, loss))
1 [Test] score/loss: 0.7400 /0.7366
可视化观察类别划分结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 x1, x2 = paddle.meshgrid(paddle.linspace(-3.5 , 2 , 200 ), paddle.linspace(-4.5 , 3.5 , 200 )) x = paddle.stack([paddle.flatten(x1), paddle.flatten(x2)], axis=1 ) y = runner.predict(x) y = paddle.argmax(y, axis=1 ) plt.ylabel('x2' ) plt.xlabel('x1' ) plt.scatter(x[:,0 ].tolist(), x[:,1 ].tolist(), c=y.tolist(), cmap=plt.cm.Spectral) paddle.seed(102 ) n_samples = 1000 X, y = make_multiclass_classification(n_samples=n_samples, n_features=2 , n_classes=3 , noise=0.2 ) plt.scatter(X[:, 0 ].tolist(), X[:, 1 ].tolist(), marker='*' , c=y.tolist())
拓展
提前停止是在使用梯度下降法进行模型优化时常用的正则化方法。对于某些拟合能力非常强的机器学习算法,当训练轮数较多时,容易发生过拟合现象,即在训练集上错误率很低,但是在未知数据(或测试集)上错误率很高。为了解决这一问题,通常会在模型优化时,使用验证集上的错误代替期望错误。当验证集上的错误率不在下降时,就停止迭代。
在3.4.3节的实验中,模型训练过程中会按照提前停止的思想保存验证集上的最优模型。
[√] 3.3 - 实践:基于Softmax回归完成鸢尾花分类任务 在本节,我们用入门深度学习的基础实验之一“鸢尾花分类任务”来进行实践,使用经典学术数据集Iris作为训练数据,实现基于Softmax回归的鸢尾花分类任务。
实践流程主要包括以下7个步骤:数据处理、模型构建、损失函数定义、优化器构建、模型训练、模型评价和模型预测等,
数据处理:根据网络接收的数据格式,完成相应的预处理操作,保证模型正常读取;
模型构建:定义Softmax回归模型类;
训练配置:训练相关的一些配置,如:优化算法、评价指标等;
组装Runner类:Runner用于管理模型训练和测试过程;
模型训练和测试:利用Runner进行模型训练、评价和测试。
说明:
使用深度学习进行实践时的操作流程基本一致,后文不再赘述。
本实践的主要配置如下:
数据:Iris数据集;
模型:Softmax回归模型;
损失函数:交叉熵损失;
优化器:梯度下降法;
评价指标:准确率。
[√] 3.3.1 - 数据处理 [√] 3.3.1.1 - 数据集介绍
输入特征维度:4
输出类别维度:3
鸢尾花属性类别对应预览
鸢尾花属性
属性1
属性2
属性3
属性4
sepal_length
sepal_width
petal_length
petal_width
花萼长度
花萼宽度
花瓣长度
花瓣宽度
鸢尾花类别
| 英文名 | 中文名 | 标签 |
| ------ | ------ | ---- |
| | | |
| :--: | :--: | :--: |
| ---- | ---- | ---- |
| | | |
|Setosa Iris |狗尾草鸢尾 | 0
|Versicolour Iris |杂色鸢尾 | 1
|Virginica Iris |
鸢尾花属性类别对应预览
sepal_length
sepal_width
petal_length
petal_width
species
5.1
3.5
1.4
0.2
setosa
4.9
3
1.4
0.2
setosa
4.7
3.2
1.3
0.2
setosa
…
…
…
…
…
[√] 3.3.1.2 - 数据清洗
对数据集中的缺失值或异常值等情况进行分析和处理,保证数据可以被模型正常读取。代码实现如下:
1 2 3 4 5 6 7 8 from sklearn.datasets import load_irisimport pandasimport numpy as np iris_features = np.array(load_iris().data, dtype=np.float32) iris_labels = np.array(load_iris().target, dtype=np.int32)print (pandas.isna(iris_features).sum ())print (pandas.isna(iris_labels).sum ())
从输出结果看,鸢尾花数据集中不存在缺失值的情况。
通过箱线图直观的显示数据分布,并观测数据中的异常值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import matplotlib.pyplot as plt def boxplot (features ): feature_names = ['sepal_length' , 'sepal_width' , 'petal_length' , 'petal_width' ] plt.figure(figsize=(5 , 5 ), dpi=200 ) plt.subplots_adjust(wspace=0.6 ) for i in range (4 ): plt.subplot(2 , 2 , i+1 ) plt.boxplot(features[:, i], showmeans=True , whiskerprops={"color" :"#E20079" , "linewidth" :0.4 , 'linestyle' :"--" }, flierprops={"markersize" :0.4 }, meanprops={"markersize" :1 }) plt.title(feature_names[i], fontdict={"size" :5 }, pad=2 ) plt.yticks(fontsize=4 , rotation=90 ) plt.tick_params(pad=0.5 ) plt.xticks([]) plt.savefig('ml-vis.pdf' ) plt.show() boxplot(iris_features)
从输出结果看,数据中基本不存在异常值,所以不需要进行异常值处理。
[√] 3.3.1.3 - 数据读取 本实验中将数据集划分为了三个部分:
训练集:用于确定模型参数;
验证集:与训练集独立的样本集合,用于使用提前停止策略选择最优模型;
测试集:用于估计应用效果(没有在模型中应用过的数据,更贴近模型在真实场景应用的效果)。
在本实验中,将$80%$的数据用于模型训练,$10%$的数据用于模型验证,$10%$的数据用于模型测试。代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import copyimport paddle def load_data (shuffle=True ): """ 加载鸢尾花数据 输入: - shuffle:是否打乱数据,数据类型为bool 输出: - X:特征数据,shape=[150,4] - y:标签数据, shape=[150] """ X = np.array(load_iris().data, dtype=np.float32) y = np.array(load_iris().target, dtype=np.int32) X = paddle.to_tensor(X) y = paddle.to_tensor(y) print ('---------------------------start' ) print (X[:5 ]) print ('---------------------------end\n' ) X_min = paddle.min (X, axis=0 ) X_max = paddle.max (X, axis=0 ) X = (X-X_min) / (X_max-X_min) print ('---------------------------start' ) print (X[:5 ]) print ('---------------------------end\n' ) if shuffle: idx = paddle.randperm(X.shape[0 ]) print ('---------------------------start' ) print (idx) print ('---------------------------end\n' ) X = X[idx] y = y[idx] return X, y paddle.seed(102 ) num_train = 120 num_dev = 15 num_test = 15 X, y = load_data(shuffle=True )print ("X shape: " , X.shape, "y shape: " , y.shape) X_train, y_train = X[:num_train], y[:num_train] X_dev, y_dev = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev] X_test, y_test = X[num_train + num_dev:], y[num_train + num_dev:]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 ---------------------------start Tensor(shape=[5 , 4 ], dtype=float32, place=CPUPlace, stop_gradient=True , [[5.09999990 , 3.50000000 , 1.39999998 , 0.20000000 ], [4.90000010 , 3. , 1.39999998 , 0.20000000 ], [4.69999981 , 3.20000005 , 1.29999995 , 0.20000000 ], [4.59999990 , 3.09999990 , 1.50000000 , 0.20000000 ], [5. , 3.59999990 , 1.39999998 , 0.20000000 ]]) ---------------------------end ---------------------------start Tensor(shape=[5 , 4 ], dtype=float32, place=CPUPlace, stop_gradient=True , [[0.22222215 , 0.62500000 , 0.06779660 , 0.04166666 ], [0.16666664 , 0.41666666 , 0.06779660 , 0.04166666 ], [0.11111101 , 0.50000000 , 0.05084745 , 0.04166666 ], [0.08333325 , 0.45833328 , 0.08474576 , 0.04166666 ], [0.19444440 , 0.66666663 , 0.06779660 , 0.04166666 ]]) ---------------------------end ---------------------------start Tensor(shape=[150 ], dtype=int64, place=CPUPlace, stop_gradient=True , [21 , 60 , 77 , 4 , 85 , 14 , 130 , 24 , 68 , 133 , 109 , 117 , 38 , 99 , 78 , 37 , 29 , 70 , 144 , 22 , 136 , 50 , 100 , 64 , 132 , 35 , 118 , 67 , 81 , 89 , 72 , 48 , 52 , 94 , 79 , 44 , 23 , 123 , 34 , 116 , 53 , 6 , 41 , 1 , 27 , 62 , 80 , 87 , 88 , 139 , 75 , 5 , 112 , 91 , 56 , 104 , 142 , 31 , 47 , 127 , 101 , 9 , 26 , 124 , 98 , 103 , 32 , 108 , 73 , 57 , 30 , 114 , 128 , 120 , 149 , 134 , 71 , 43 , 45 , 46 , 55 , 146 , 90 , 12 , 83 , 74 , 0 , 33 , 82 , 115 , 65 , 15 , 140 , 20 , 69 , 11 , 131 , 107 , 148 , 19 , 61 , 93 , 59 , 51 , 141 , 105 , 16 , 54 , 110 , 145 , 137 , 135 , 28 , 10 , 119 , 63 , 122 , 121 , 84 , 17 , 96 , 138 , 39 , 129 , 76 , 95 , 2 , 113 , 147 , 102 , 86 , 126 , 49 , 18 , 7 , 3 , 125 , 92 , 25 , 66 , 40 , 58 , 36 , 106 , 111 , 97 , 42 , 143 , 13 , 8 ]) ---------------------------end X shape: [150 , 4 ] y shape: [150 ]
1 2 print ("X_train shape: " , X_train.shape, "y_train shape: " , y_train.shape)
1 X_train shape: [120 , 4 ] y_train shape: [120 ]
1 2 Tensor(shape=[5 ], dtype=int32, place=CPUPlace, stop_gradient=True , [0 , 1 , 1 , 0 , 1 ])
[√] 3.3.2 - 模型构建 使用Softmax回归模型进行鸢尾花分类实验,将模型的输入维度定义为4,输出维度定义为3。代码实现如下:
1 2 3 4 5 6 7 8 from nndl import op input_dim = 4 output_dim = 3 model = op.model_SR(input_dim=input_dim, output_dim=output_dim)
[√] 3.3.3 - 模型训练 实例化RunnerV2类,使用训练集和验证集进行模型训练,共训练80个epoch,其中每隔10个epoch打印训练集上的指标,并且保存准确率最高的模型作为最佳模型。代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from nndl import op, metric, opitimizer, RunnerV2 lr = 0.2 optimizer = opitimizer.SimpleBatchGD(init_lr=lr, model=model) loss_fn = op.MultiCrossEntropyLoss() metric = metric.accuracy runner = RunnerV2(model, optimizer, metric, loss_fn) runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=200 , log_epochs=10 , save_path="best_model.pdparams" )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 best accuracy performence has been updated: 0.00000 --> 0.86667 [Train] epoch: 0 , loss: 0.48671841621398926 , score: 0.8916666507720947 [Dev] epoch: 0 , loss: 0.511984646320343 , score: 0.8666666746139526 [Train] epoch: 20 , loss: 0.4710509479045868 , score: 0.8916666507720947 [Dev] epoch: 20 , loss: 0.4989580810070038 , score: 0.8666666746139526 [Train] epoch: 40 , loss: 0.45711272954940796 , score: 0.8999999761581421 [Dev] epoch: 40 , loss: 0.4874427020549774 , score: 0.8666666746139526 [Train] epoch: 60 , loss: 0.4445931017398834 , score: 0.8999999761581421 [Dev] epoch: 60 , loss: 0.47714489698410034 , score: 0.8666666746139526 [Train] epoch: 80 , loss: 0.43325552344322205 , score: 0.9166666865348816 [Dev] epoch: 80 , loss: 0.4678438603878021 , score: 0.8666666746139526 best accuracy performence has been updated: 0.86667 --> 0.93333 [Train] epoch: 100 , loss: 0.4229157567024231 , score: 0.9166666865348816 [Dev] epoch: 100 , loss: 0.4593701660633087 , score: 0.9333333373069763 [Train] epoch: 120 , loss: 0.41342711448669434 , score: 0.9166666865348816 [Dev] epoch: 120 , loss: 0.45159193873405457 , score: 0.9333333373069763 [Train] epoch: 140 , loss: 0.4046727418899536 , score: 0.9166666865348816 [Dev] epoch: 140 , loss: 0.4444047808647156 , score: 0.9333333373069763 [Train] epoch: 160 , loss: 0.3965565264225006 , score: 0.9166666865348816 [Dev] epoch: 160 , loss: 0.4377250373363495 , score: 0.9333333373069763 [Train] epoch: 180 , loss: 0.38899996876716614 , score: 0.9166666865348816 [Dev] epoch: 180 , loss: 0.43148478865623474 , score: 0.9333333373069763 [Train] epoch: 200 , loss: 0.3819374144077301 , score: 0.9166666865348816 [Dev] epoch: 200 , loss: 0.42562830448150635 , score: 0.9333333373069763 [Train] epoch: 220 , loss: 0.3753131628036499 , score: 0.9166666865348816 [Dev] epoch: 220 , loss: 0.42010951042175293 , score: 0.9333333373069763 [Train] epoch: 240 , loss: 0.369081050157547 , score: 0.9166666865348816 [Dev] epoch: 240 , loss: 0.41488999128341675 , score: 0.9333333373069763 [Train] epoch: 260 , loss: 0.36320066452026367 , score: 0.925000011920929 [Dev] epoch: 260 , loss: 0.40993720293045044 , score: 0.9333333373069763 [Train] epoch: 280 , loss: 0.3576377034187317 , score: 0.925000011920929 [Dev] epoch: 280 , loss: 0.4052235782146454 , score: 0.9333333373069763 [Train] epoch: 300 , loss: 0.35236233472824097 , score: 0.925000011920929 [Dev] epoch: 300 , loss: 0.4007255733013153 , score: 0.9333333373069763 [Train] epoch: 320 , loss: 0.34734851121902466 , score: 0.925000011920929 [Dev] epoch: 320 , loss: 0.3964228332042694 , score: 0.9333333373069763 [Train] epoch: 340 , loss: 0.34257346391677856 , score: 0.925000011920929 [Dev] epoch: 340 , loss: 0.3922978341579437 , score: 0.9333333373069763 [Train] epoch: 360 , loss: 0.33801719546318054 , score: 0.925000011920929 [Dev] epoch: 360 , loss: 0.388335257768631 , score: 0.9333333373069763 [Train] epoch: 380 , loss: 0.3336619734764099 , score: 0.925000011920929 [Dev] epoch: 380 , loss: 0.3845216929912567 , score: 0.9333333373069763
可视化观察训练集与验证集的准确率变化情况。
1 2 3 from nndl import plot plot(runner,fig_name='linear-acc3.pdf' )
[√] 3.3.4 - 模型评价 使用测试数据对在训练过程中保存的最佳模型进行评价,观察模型在测试集上的准确率情况。代码实现如下:
1 2 3 4 5 runner.load_model('best_model.pdparams' ) score, loss = runner.evaluate([X_test, y_test])print ("[Test] score/loss: {:.4f}/{:.4f}" .format (score, loss))
1 [Test] score/loss: 0.9333 /0.4093
[√] 3.3.5 - 模型预测 使用保存好的模型,对测试集中的数据进行模型预测,并取出1条数据观察模型效果。代码实现如下:
1 2 3 4 5 6 7 8 logits = runner.predict(X_test) pred = paddle.argmax(logits[0 ]).numpy() label = y_test[0 ].numpy()print ("The true category is {} and the predicted category is {}" .format (label[0 ], pred[0 ]))
1 The true category is 0 and the predicted category is 0
[√] 3.4 - 小结 本节实现了Logistic回归和Softmax回归两种基本的线性分类模型。
[√] 3.5 - 实验拓展 为了加深对机器学习模型的理解,请自己动手完成以下实验:
尝试调整学习率和训练轮数等超参数,观察是否能够得到更高的精度;
在解决多分类问题时,还有一个思路是将每个类别的求解问题拆分成一个二分类任务,通过判断是否属于该类别来判断最终结果。请分别尝试两种求解思路,观察哪种能够取得更好的结果;
尝试使用《神经网络与深度学习》中的其他模型进行鸢尾花识别任务,观察是否能够得到更高的精度。