• 基于Softmax回归的多分类任务


            Logistic回归可以有效地解决二分类问题,但在分类任务中,还有一类多分类问题,即类别数C大于2 的分类问题。Softmax回归就是Logistic回归在多分类问题上的推广。

            使用Softmax回归模型对一个简单的数据集进行多分类实验。

            首先给大家看一下需要的资源包

    代码最后都会放出。

    1.数据集的构建

            我们首先构建一个简单的多分类任务,并构建训练集、验证集和测试集。

            本任务的数据来自3个不同的簇,每个簇对一个类别。我们采集1000条样本,每个样本包含2个特征。

            数据集的构建函数make_multiclass_classification的代码实现如下:

    1. def make_multiclass_classification(n_samples=100, n_features=2, n_classes=3, shuffle=True, noise=0.1):
    2. """
    3. 生成带噪音的多类别数据
    4. 输入:
    5. - n_samples:数据量大小,数据类型为int
    6. - n_features:特征数量,数据类型为int
    7. - shuffle:是否打乱数据,数据类型为bool
    8. - noise:以多大的程度增加噪声,数据类型为None或float,noise为None时表示不增加噪声
    9. 输出:
    10. - X:特征数据,shape=[n_samples,2]
    11. - y:标签数据, shape=[n_samples,1]
    12. """
    13. # 计算每个类别的样本数量
    14. n_samples_per_class = [int(n_samples / n_classes) for k in range(n_classes)]
    15. for i in range(n_samples - sum(n_samples_per_class)):
    16. n_samples_per_class[i % n_classes] += 1
    17. # 将特征和标签初始化为0
    18. X = torch.zeros((n_samples, n_features))
    19. y = torch.zeros(n_samples, dtype=torch.int32)
    20. # 随机生成3个簇中心作为类别中心
    21. centroids = torch.randperm(2 ** n_features)[:n_classes]
    22. centroids_bin = np.unpackbits(centroids.numpy().astype('uint8')).reshape((-1, 8))[:, -n_features:]
    23. centroids = torch.tensor(centroids_bin, dtype=torch.float32)
    24. # 控制簇中心的分离程度
    25. centroids = 1.5 * centroids - 1
    26. # 随机生成特征值
    27. X[:, :n_features] = torch.randn((n_samples, n_features))
    28. stop = 0
    29. # 将每个类的特征值控制在簇中心附近
    30. for k, centroid in enumerate(centroids):
    31. start, stop = stop, stop + n_samples_per_class[k]
    32. # 指定标签值
    33. y[start:stop] = k % n_classes
    34. X_k = X[start:stop, :n_features]
    35. # 控制每个类别特征值的分散程度
    36. A = 2 * torch.rand(size=(n_features, n_features)) - 1
    37. X_k[...] = torch.matmul(X_k, A)
    38. X_k += centroid
    39. X[start:stop, :n_features] = X_k
    40. # 如果noise不为None,则给特征加入噪声
    41. if noise > 0.0:
    42. # 生成noise掩膜,用来指定给那些样本加入噪声
    43. noise_mask = torch.rand(n_samples) < noise
    44. for i in range(len(noise_mask)):
    45. if noise_mask[i]:
    46. # 给加噪声的样本随机赋标签值
    47. y[i] = torch.randint(n_classes, size=(1,), dtype=torch.int32)
    48. # 如果shuffle为True,将所有数据打乱
    49. if shuffle:
    50. idx = torch.randperm(X.shape[0])
    51. X = X[idx]
    52. y = y[idx]
    53. return X, y

                     随机采集1000个样本,并进行可视化。

    1. # 采样1000个样本
    2. n_samples = 1000
    3. X, y = make_multiclass_classification(n_samples=n_samples, n_features=2, n_classes=3, noise=0.2)
    4. # 可视化生产的数据集,不同颜色代表不同类别
    5. plt.figure(figsize=(5,5))
    6. plt.scatter(x=X[:, 0].tolist(), y=X[:, 1].tolist(), marker='*', c=y.tolist())
    7. plt.savefig('linear-dataset-vis2.pdf')
    8. plt.show()

             运行结果如下:

            将实验数据拆分成训练集、验证集和测试集。其中训练集640条、验证集160条、测试集200条。

    1. num_train = 640
    2. num_dev = 160
    3. num_test = 200
    4. X_train, y_train = X[:num_train], y[:num_train]
    5. X_dev, y_dev = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev]
    6. X_test, y_test = X[num_train + num_dev:], y[num_train + num_dev:]
    7. # 打印X_train和y_train的维度
    8. print("X_train shape: ", X_train.shape, "y_train shape: ", y_train.shape)

            这样,我们就完成了Multi1000数据集的构建。

    1. # 打印前5个数据的标签
    2. print(y_train[:5])

             运行结果如下:

    2.模型构建

            在Softmax回归中,对类别进行预测的方式是预测输入属于每个类别的条件概率。与Logistic 回归不同的是,Softmax回归的输出值个数等于类别数C,而每个类别的概率值则通过Softmax函数进行求解。


    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)}

    在Softmax函数的计算过程中,要注意上溢出下溢出的问题。假设Softmax 函数中所有的$x_k$都是相同大小的数值$a$,理论上,所有的输出都应该为$\frac{1}{k}$。但需要考虑如下两种特殊情况:

    • $a$为一个非常大的负数,此时$\exp(a)$ 会发生下溢出现象。计算机在进行数值计算时,当数值过小,会被四舍五入为0。此时,Softmax函数的分母会变为0,导致计算出现问题;
    • $a$为一个非常大的正数,此时会导致$\exp(a)$发生上溢出现象,导致计算出现问题。

    为了解决上溢出和下溢出的问题,在计算Softmax函数时,可以使用$x_k - \max(\mathbf x)$代替$x_k$。 此时,通过减去最大值,$x_k$最大为0,避免了上溢出的问题;同时,分母中至少会包含一个值为1的项,从而也避免了下溢出的问题。 

    Softmax函数的代码实现如下(activation.py):

    1. # x为tensor
    2. def softmax(X):
    3. """
    4. 输入:
    5. - X:shape=[N, C],N为向量数量,C为向量维度
    6. """
    7. x_max = paddle.max(X, axis=1, keepdim=True)#N,1
    8. x_exp = paddle.exp(X - x_max)
    9. partition = paddle.sum(x_exp, axis=1, keepdim=True)#N,1
    10. return x_exp / partition
    11. # 观察softmax的计算方式
    12. X = paddle.to_tensor([[0.1, 0.2, 0.3, 0.4],[1,2,3,4]])
    13. predict = softmax(X)
    14. print(predict)

             运行结果如下:


    soft回归算子

    在Softmax回归中,类别标签y\epsilon\left \{ 1,2,...,C \right \}。给定一个样本$\mathbf x$,使用Softmax回归预测的属于类别$c$的条件概率为

    p(y=c|x)=softmax(w_{c}^{T}x+b_c)

    其中w_c是第c类的权重向量,b_c是第c类的偏置。

    Softmax回归模型其实就是线性模型与softmax函数的组合。

    将N个样本归为一组进行成批的预测。

            \hat{\mathbf Y} = \mathrm{softmax}(\boldsymbol{X} \boldsymbol{W} + \mathbf b)

    其中$\boldsymbol{X}\in \mathbb{R}^{N\times D}$为N个样本的特征矩阵,W = \left [ w_1,......,w_c \right ]$C$个类的权重向量组成的矩阵,$\hat{\mathbf Y}\in \mathbb{R}^{C}$为所有类别的预测条件概率组成的矩阵。

    我们根据公式(3.13)实现Softmax回归算子,代码实现如下:

    1. class model_SR(Op):
    2. def __init__(self, input_dim, output_dim):
    3. super(model_SR, self).__init__()
    4. self.params = {}
    5. #将线性层的权重参数全部初始化为0
    6. self.params['W'] = torch.zeros((input_dim, output_dim))
    7. #self.params['W'] = paddle.normal(mean=0, std=0.01, shape=[input_dim, output_dim])
    8. #将线性层的偏置参数初始化为0
    9. self.params['b'] = torch.zeros(output_dim)
    10. #存放参数的梯度
    11. self.grads = {}
    12. self.X = None
    13. self.outputs = None
    14. self.output_dim = output_dim
    15. def __call__(self, inputs):
    16. return self.forward(inputs)
    17. def forward(self, inputs):
    18. self.X = inputs
    19. #线性计算
    20. score = torch.matmul(self.X, self.params['W']) + self.params['b']
    21. #Softmax 函数
    22. self.outputs = softmax(score)
    23. return self.outputs
    24. # 随机生成1条长度为4的数据
    25. inputs = paddle.randn(shape=[1,4])
    26. print('Input is:', inputs)
    27. # 实例化模型,这里令输入长度为4,输出类别数为3
    28. model = model_SR(input_dim=4, output_dim=3)
    29. outputs = model(inputs)
    30. print('Output is:', outputs)

              运行结果如下:

    从输出结果可以看出,采用全0初始化后,属于每个类别的条件概率均为$\frac{1}{C}$。这是因为,不论输入值的大小为多少,线性函数$f(x;W,b)$的输出值恒为0。此时,再经过Softmax函数的处理,每个类别的条件概率恒等。

    3.损失函数

    Softmax回归同样使用交叉熵损失作为损失函数,并使用梯度下降法对参数进行优化。通常使用$C$维的one-hot类型向量$y{0,1}C$来表示多分类任务中的类别标签。对于类别$c$,其向量表示为:

    y=[I(1=c),I(2=c),...,I(C=c)]T 

    其中$I()$是指示函数,即括号内的输入为“真”,$I()=1$;否则,$I()=0$

    给定有$N$个训练样本的训练集${(x(n),y(n))}Nn=1$,令$ˆy(n)=softmax(WTx(n)+b)$为样本$x(n)$在每个类别的后验概率。多分类问题的交叉熵损失函数定义为:

    观察上式,$y(n)c$$c$为真实类别时为1,其余都为0。也就是说,交叉熵损失只关心正确类别的预测概率,因此,上式又可以优化为:

    其中$y(n)$是第$n$个样本的标签。

    因此,多类交叉熵损失函数的代码实现如下:

    1. class MultiCrossEntropyLoss(Op):
    2. def __init__(self):
    3. self.predicts = None
    4. self.labels = None
    5. self.num = None
    6. def __call__(self, predicts, labels):
    7. return self.forward(predicts, labels)
    8. def forward(self, predicts, labels):
    9. """
    10. 输入:
    11. - predicts:预测值,shape=[N, 1],N为样本数量
    12. - labels:真实标签,shape=[N, 1]
    13. 输出:
    14. - 损失值:shape=[1]
    15. """
    16. self.predicts = predicts
    17. self.labels = labels
    18. self.num = self.predicts.shape[0]
    19. loss = 0
    20. for i in range(0, self.num):
    21. index = self.labels[i]
    22. loss -= torch.log(self.predicts[i][index])
    23. return loss / self.num
    24. # 测试一下
    25. # 假设真实标签为第1类
    26. labels = paddle.to_tensor([0])
    27. # 计算风险函数
    28. mce_loss = MultiCrossEntropyLoss()
    29. print(mce_loss(outputs, labels))

              运行结果如下:

    4.模型优化

    计算风险函数$R(W,b)$关于参数$W$$b$的偏导数。在Softmax回归中,计算方法为:

    R(W,b)W=1NNn=1x(n)(y(n)ˆy(n))T=1NXT(yˆy)

    R(W,b)b=1NNn=1(y(n)ˆy(n))T=1N1(yˆy)

    其中$XRN×D$$N$个样本组成的矩阵,$yRN$$N$个样本标签组成的向量,$ˆyRN$$N$个样本的预测标签组成的向量,$1$$N$维的全1向量。

    将上述计算方法定义在模型的backward函数中,代码实现如下:

    1. class model_SR(Op):
    2. def __init__(self, input_dim, output_dim):
    3. super(model_SR, self).__init__()
    4. self.params = {}
    5. #将线性层的权重参数全部初始化为0
    6. self.params['W'] = torch.zeros((input_dim, output_dim))
    7. #self.params['W'] = paddle.normal(mean=0, std=0.01, shape=[input_dim, output_dim])
    8. #将线性层的偏置参数初始化为0
    9. self.params['b'] = torch.zeros(output_dim)
    10. #存放参数的梯度
    11. self.grads = {}
    12. self.X = None
    13. self.outputs = None
    14. self.output_dim = output_dim
    15. def __call__(self, inputs):
    16. return self.forward(inputs)
    17. def forward(self, inputs):
    18. self.X = inputs
    19. #线性计算
    20. score = torch.matmul(self.X, self.params['W']) + self.params['b']
    21. #Softmax 函数
    22. self.outputs = softmax(score)
    23. return self.outputs
    24. def backward(self, labels):
    25. """
    26. 输入:
    27. - labels:真实标签,shape=[N, 1],其中N为样本数量
    28. """
    29. #计算偏导数
    30. N =labels.size()[0]
    31. labels = torch.nn.functional.one_hot(labels.to(torch.int64), self.output_dim)
    32. self.grads['W'] = -1 / N * torch.matmul(self.X.t(), (labels-self.outputs))
    33. self.grads['b'] = -1 / N * torch.matmul(torch.ones(N), (labels-self.outputs))

    5.模型训练

    1. # 特征维度
    2. input_dim = 2
    3. # 类别数
    4. output_dim = 3
    5. # 学习率
    6. lr = 0.1
    7. # 实例化模型
    8. model = model_SR(input_dim=input_dim, output_dim=output_dim)
    9. # 指定优化器
    10. optimizer = SimpleBatchGD(init_lr=lr, model=model)
    11. # 指定损失函数
    12. loss_fn = MultiCrossEntropyLoss()
    13. # 指定评价方式
    14. metric = accuracy
    15. # 实例化RunnerV2类
    16. runner = RunnerV2(model, optimizer, metric, loss_fn)
    17. # 模型训练
    18. runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=500, log_eopchs=50, eval_epochs=1, save_path="best_model.pdparams")
    19. # 可视化观察训练集与验证集的准确率变化情况
    20. plot(runner,fig_name='linear-acc2.pdf')

               运行结果如下:

     6.模型评价

    1. score, loss = runner.evaluate([X_test, y_test])
    2. print("[Test] score/loss: {:.4f}/{:.4f}".format(score, loss))

               运行结果如下:

    1. # 均匀生成40000个数据点
    2. x1, x2 = torch.meshgrid(torch.linspace(-3.5, 2, 200), torch.linspace(-4.5, 3.5, 200))
    3. x = torch.stack([torch.flatten(x1), torch.flatten(x2)], dim=1)
    4. # 预测对应类别
    5. y = runner.predict(x)
    6. y = torch.argmax(y, dim=1)
    7. # 绘制类别区域
    8. plt.ylabel('x2')
    9. plt.xlabel('x1')
    10. plt.scatter(x[:,0].tolist(), x[:,1].tolist(), c=y.tolist(), cmap=plt.cm.Spectral)
    11. n_samples = 1000
    12. X, y = make_multiclass_classification(n_samples=n_samples, n_features=2, n_classes=3, noise=0.2)
    13. plt.scatter(X[:, 0].tolist(), X[:, 1].tolist(), marker='*', c=y.tolist())
    14. plt.legend()
    15. plt.show()

             运行结果如下:

    附录:

    main.py 

    1. import numpy as np
    2. import torch
    3. import matplotlib.pyplot as plt
    4. from nndl.op import model_SR
    5. from nndl.activation import softmax
    6. from nndl.op import MultiCrossEntropyLoss
    7. from nndl.opitimizer import SimpleBatchGD
    8. from nndl.metric import accuracy
    9. from nndl.runner import RunnerV2
    10. from nndl.tools import plot
    11. def make_multiclass_classification(n_samples=100, n_features=2, n_classes=3, shuffle=True, noise=0.1):
    12. """
    13. 生成带噪音的多类别数据
    14. 输入:
    15. - n_samples:数据量大小,数据类型为int
    16. - n_features:特征数量,数据类型为int
    17. - shuffle:是否打乱数据,数据类型为bool
    18. - noise:以多大的程度增加噪声,数据类型为None或float,noise为None时表示不增加噪声
    19. 输出:
    20. - X:特征数据,shape=[n_samples,2]
    21. - y:标签数据, shape=[n_samples,1]
    22. """
    23. # 计算每个类别的样本数量
    24. n_samples_per_class = [int(n_samples / n_classes) for k in range(n_classes)]
    25. for i in range(n_samples - sum(n_samples_per_class)):
    26. n_samples_per_class[i % n_classes] += 1
    27. # 将特征和标签初始化为0
    28. X = torch.zeros((n_samples, n_features))
    29. y = torch.zeros(n_samples, dtype=torch.int32)
    30. # 随机生成3个簇中心作为类别中心
    31. centroids = torch.randperm(2 ** n_features)[:n_classes]
    32. centroids_bin = np.unpackbits(centroids.numpy().astype('uint8')).reshape((-1, 8))[:, -n_features:]
    33. centroids = torch.tensor(centroids_bin, dtype=torch.float32)
    34. # 控制簇中心的分离程度
    35. centroids = 1.5 * centroids - 1
    36. # 随机生成特征值
    37. X[:, :n_features] = torch.randn((n_samples, n_features))
    38. stop = 0
    39. # 将每个类的特征值控制在簇中心附近
    40. for k, centroid in enumerate(centroids):
    41. start, stop = stop, stop + n_samples_per_class[k]
    42. # 指定标签值
    43. y[start:stop] = k % n_classes
    44. X_k = X[start:stop, :n_features]
    45. # 控制每个类别特征值的分散程度
    46. A = 2 * torch.rand(size=(n_features, n_features)) - 1
    47. X_k[...] = torch.matmul(X_k, A)
    48. X_k += centroid
    49. X[start:stop, :n_features] = X_k
    50. # 如果noise不为None,则给特征加入噪声
    51. if noise > 0.0:
    52. # 生成noise掩膜,用来指定给那些样本加入噪声
    53. noise_mask = torch.rand(n_samples) < noise
    54. for i in range(len(noise_mask)):
    55. if noise_mask[i]:
    56. # 给加噪声的样本随机赋标签值
    57. y[i] = torch.randint(n_classes, size=(1,), dtype=torch.int32)
    58. # 如果shuffle为True,将所有数据打乱
    59. if shuffle:
    60. idx = torch.randperm(X.shape[0])
    61. X = X[idx]
    62. y = y[idx]
    63. return X, y
    64. # 采样1000个样本
    65. n_samples = 1000
    66. X, y = make_multiclass_classification(n_samples=n_samples, n_features=2, n_classes=3, noise=0.2)
    67. # 可视化生产的数据集,不同颜色代表不同类别
    68. plt.figure(figsize=(5,5))
    69. plt.scatter(x=X[:, 0].tolist(), y=X[:, 1].tolist(), marker='*', c=y.tolist())
    70. plt.savefig('linear-dataset-vis2.pdf')
    71. plt.show()
    72. num_train = 640
    73. num_dev = 160
    74. num_test = 200
    75. X_train, y_train = X[:num_train], y[:num_train]
    76. X_dev, y_dev = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev]
    77. X_test, y_test = X[num_train + num_dev:], y[num_train + num_dev:]
    78. # 打印X_train和y_train的维度
    79. print("X_train shape: ", X_train.shape, "y_train shape: ", y_train.shape)
    80. # 打印前5个数据的标签
    81. print(y_train[:5])
    82. # 观察softmax的计算方式
    83. X = torch.tensor([[0.1, 0.2, 0.3, 0.4],[1,2,3,4]], dtype=torch.float32)
    84. predict = softmax(X)
    85. print(predict)
    86. # 随机生成1条长度为4的数据
    87. inputs = torch.randn(size=(1, 4))
    88. print('Input is:', inputs)
    89. # 实例化模型,这里令输入长度为4,输出类别数为3
    90. model = model_SR(input_dim=4, output_dim=3)
    91. outputs = model(inputs)
    92. print('Output is:', outputs)
    93. labels = torch.tensor([0])
    94. # 计算风险函数
    95. mce_loss = MultiCrossEntropyLoss()
    96. print(mce_loss(outputs, labels))
    97. # 特征维度
    98. input_dim = 2
    99. # 类别数
    100. output_dim = 3
    101. # 学习率
    102. lr = 0.1
    103. # 实例化模型
    104. model = model_SR(input_dim=input_dim, output_dim=output_dim)
    105. # 指定优化器
    106. optimizer = SimpleBatchGD(init_lr=lr, model=model)
    107. # 指定损失函数
    108. loss_fn = MultiCrossEntropyLoss()
    109. # 指定评价方式
    110. metric = accuracy
    111. # 实例化RunnerV2类
    112. runner = RunnerV2(model, optimizer, metric, loss_fn)
    113. # 模型训练
    114. runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=500, log_eopchs=50, eval_epochs=1, save_path="best_model.pdparams")
    115. # 可视化观察训练集与验证集的准确率变化情况
    116. plot(runner,fig_name='linear-acc2.pdf')
    117. score, loss = runner.evaluate([X_test, y_test])
    118. print("[Test] score/loss: {:.4f}/{:.4f}".format(score, loss))
    119. # 均匀生成40000个数据点
    120. x1, x2 = torch.meshgrid(torch.linspace(-3.5, 2, 200), torch.linspace(-4.5, 3.5, 200))
    121. x = torch.stack([torch.flatten(x1), torch.flatten(x2)], dim=1)
    122. # 预测对应类别
    123. y = runner.predict(x)
    124. y = torch.argmax(y, dim=1)
    125. # 绘制类别区域
    126. plt.ylabel('x2')
    127. plt.xlabel('x1')
    128. plt.scatter(x[:,0].tolist(), x[:,1].tolist(), c=y.tolist(), cmap=plt.cm.Spectral)
    129. n_samples = 1000
    130. X, y = make_multiclass_classification(n_samples=n_samples, n_features=2, n_classes=3, noise=0.2)
    131. plt.scatter(X[:, 0].tolist(), X[:, 1].tolist(), marker='*', c=y.tolist())
    132. plt.legend()
    133. plt.show()

    nndl包

    op.py 

    1. import torch
    2. from DL.实验4_2.nndl.activation import softmax
    3. torch.seed() #设置随机种子
    4. class Op(object):
    5. def __init__(self):
    6. pass
    7. def __call__(self, inputs):
    8. return self.forward(inputs)
    9. def forward(self, inputs):
    10. raise NotImplementedError
    11. def backward(self, inputs):
    12. raise NotImplementedError
    13. # 线性算子
    14. class Linear(Op):
    15. def __init__(self,dimension):
    16. """
    17. 输入:
    18. - dimension:模型要处理的数据特征向量长度
    19. """
    20. self.dim = dimension
    21. # 模型参数
    22. self.params = {}
    23. self.params['w'] = torch.randn(self.dim,1,dtype=torch.float32)
    24. self.params['b'] = torch.zeros(1,dtype=torch.float32)
    25. def __call__(self, X):
    26. return self.forward(X)
    27. # 前向函数
    28. def forward(self, X):
    29. """
    30. 输入:
    31. - X: tensor, shape=[N,D]
    32. 注意这里的X矩阵是由N个x向量的转置拼接成的,与原教材行向量表示方式不一致
    33. 输出:
    34. - y_pred: tensor, shape=[N]
    35. """
    36. N,D = X.shape
    37. if self.dim==0:
    38. return torch.full((N, 1), self.params['b'])
    39. assert D==self.dim # 输入数据维度合法性验证
    40. # 使用paddle.matmul计算两个tensor的乘积
    41. y_pred = torch.matmul(X,self.params['w'])+self.params['b']
    42. return y_pred
    43. #新增Softmax算子
    44. class model_SR(Op):
    45. def __init__(self, input_dim, output_dim):
    46. super(model_SR, self).__init__()
    47. self.params = {}
    48. #将线性层的权重参数全部初始化为0
    49. self.params['W'] = torch.zeros((input_dim, output_dim))
    50. #self.params['W'] = paddle.normal(mean=0, std=0.01, shape=[input_dim, output_dim])
    51. #将线性层的偏置参数初始化为0
    52. self.params['b'] = torch.zeros(output_dim)
    53. #存放参数的梯度
    54. self.grads = {}
    55. self.X = None
    56. self.outputs = None
    57. self.output_dim = output_dim
    58. def __call__(self, inputs):
    59. return self.forward(inputs)
    60. def forward(self, inputs):
    61. self.X = inputs
    62. #线性计算
    63. score = torch.matmul(self.X, self.params['W']) + self.params['b']
    64. #Softmax 函数
    65. self.outputs = softmax(score)
    66. return self.outputs
    67. def backward(self, labels):
    68. """
    69. 输入:
    70. - labels:真实标签,shape=[N, 1],其中N为样本数量
    71. """
    72. #计算偏导数
    73. N =labels.size()[0]
    74. labels = torch.nn.functional.one_hot(labels.to(torch.int64), self.output_dim)
    75. self.grads['W'] = -1 / N * torch.matmul(self.X.t(), (labels-self.outputs))
    76. self.grads['b'] = -1 / N * torch.matmul(torch.ones(N), (labels-self.outputs))
    77. #新增多类别交叉熵损失
    78. class MultiCrossEntropyLoss(Op):
    79. def __init__(self):
    80. self.predicts = None
    81. self.labels = None
    82. self.num = None
    83. def __call__(self, predicts, labels):
    84. return self.forward(predicts, labels)
    85. def forward(self, predicts, labels):
    86. """
    87. 输入:
    88. - predicts:预测值,shape=[N, 1],N为样本数量
    89. - labels:真实标签,shape=[N, 1]
    90. 输出:
    91. - 损失值:shape=[1]
    92. """
    93. self.predicts = predicts
    94. self.labels = labels
    95. self.num = self.predicts.shape[0]
    96. loss = 0
    97. for i in range(0, self.num):
    98. index = self.labels[i]
    99. loss -= torch.log(self.predicts[i][index])
    100. return loss / self.num

     activation.py

    1. import torch
    2. # x为tensor
    3. def softmax(X):
    4. """
    5. 输入:
    6. - X:shape=[N, C],N为向量数量,C为向量维度
    7. """
    8. x_max = torch.max(X, dim=1, keepdim=True) # N,1
    9. x_exp = torch.exp(X - x_max.values)
    10. partition = torch.sum(x_exp, dim=1, keepdim=True) # N,1
    11. return x_exp / partition

    opitimizer.py

    1. import torch
    2. def optimizer_lsm(model, X, y, reg_lambda=0):
    3. """
    4. 输入:
    5. - model: 模型
    6. - X: tensor, 特征数据,shape=[N,D]
    7. - y: tensor,标签数据,shape=[N]
    8. - reg_lambda: float, 正则化系数,默认为0
    9. 输出:
    10. - model: 优化好的模型
    11. """
    12. N, D = X.shape
    13. # 对输入特征数据所有特征向量求平均
    14. x_bar_tran = torch.mean(X,dim=0).T
    15. # 求标签的均值,shape=[1]
    16. y_bar = torch.mean(y)
    17. # paddle.subtract通过广播的方式实现矩阵减向量
    18. x_sub = torch.subtract(X,x_bar_tran)
    19. # 使用paddle.all判断输入tensor是否全0
    20. if torch.all(x_sub==0):
    21. model.params['b'] = y_bar
    22. model.params['w'] = torch.zeros(D)
    23. return model
    24. # paddle.inverse求方阵的逆
    25. tmp = torch.inverse(torch.matmul(x_sub.T,x_sub)+
    26. reg_lambda*torch.eye(D))
    27. w = torch.matmul(torch.matmul(tmp,x_sub.T),(y-y_bar))
    28. b = y_bar-torch.matmul(x_bar_tran,w)
    29. model.params['b'] = b
    30. model.params['w'] = torch.squeeze(w,dim=-1)
    31. return model
    32. from abc import abstractmethod
    33. #新增优化器基类
    34. class Optimizer(object):
    35. def __init__(self, init_lr, model):
    36. """
    37. 优化器类初始化
    38. """
    39. #初始化学习率,用于参数更新的计算
    40. self.init_lr = init_lr
    41. #指定优化器需要优化的模型
    42. self.model = model
    43. @abstractmethod
    44. def step(self):
    45. """
    46. 定义每次迭代如何更新参数
    47. """
    48. pass
    49. #新增梯度下降法优化器
    50. class SimpleBatchGD(Optimizer):
    51. def __init__(self, init_lr, model):
    52. super(SimpleBatchGD, self).__init__(init_lr=init_lr, model=model)
    53. def step(self):
    54. #参数更新
    55. #遍历所有参数,按照公式(3.8)和(3.9)更新参数
    56. if isinstance(self.model.params, dict):
    57. for key in self.model.params.keys():
    58. self.model.params[key] = self.model.params[key] - self.init_lr * self.model.grads[key]

    metric.py

    1. import torch
    2. def accuracy(preds, labels):
    3. """
    4. 输入:
    5. - preds:预测值,二分类时,shape=[N, 1],N为样本数量,多分类时,shape=[N, C],C为类别数量
    6. - labels:真实标签,shape=[N, 1]
    7. 输出:
    8. - 准确率:shape=[1]
    9. """
    10. # 判断是二分类任务还是多分类任务,preds.shape[1]=1时为二分类任务,preds.shape[1]>1时为多分类任务
    11. if preds.shape[1] == 1:
    12. # 二分类时,判断每个概率值是否大于0.5,当大于0.5时,类别为1,否则类别为0
    13. # 使用'torch.gt'比较preds和0.5,返回bool类型的tensor,再使用'torch.float32'将bool类型的tensor转换为float32类型的tensor
    14. preds = torch.gt(preds, 0.5).float()
    15. else:
    16. # 多分类时,使用'torch.argmax'计算最大元素索引作为类别
    17. preds = torch.argmax(preds, dim=1)
    18. return torch.mean((preds == labels).float())

    runner.py

    1. import torch
    2. # 新增RunnerV2类
    3. class RunnerV2(object):
    4. def __init__(self, model, optimizer, metric, loss_fn):
    5. self.model = model
    6. self.optimizer = optimizer
    7. self.loss_fn = loss_fn
    8. self.metric = metric
    9. # 记录训练过程中的评价指标变化情况
    10. self.train_scores = []
    11. self.dev_scores = []
    12. # 记录训练过程中的损失函数变化情况
    13. self.train_loss = []
    14. self.dev_loss = []
    15. def train(self, train_set, dev_set, **kwargs):
    16. # 传入训练轮数,如果没有传入值则默认为0
    17. num_epochs = kwargs.get("num_epochs", 0)
    18. # 传入log打印频率,如果没有传入值则默认为100
    19. log_epochs = kwargs.get("log_epochs", 100)
    20. # 传入模型保存路径,如果没有传入值则默认为"best_model.pdparams"
    21. save_path = kwargs.get("save_path", "best_model.pdparams")
    22. # 梯度打印函数,如果没有传入则默认为"None"
    23. print_grads = kwargs.get("print_grads", None)
    24. # 记录全局最优指标
    25. best_score = 0
    26. # 进行num_epochs轮训练
    27. for epoch in range(num_epochs):
    28. X, y = train_set
    29. # 获取模型预测
    30. logits = self.model(X)
    31. # 计算交叉熵损失
    32. trn_loss = self.loss_fn(logits, y).item()
    33. self.train_loss.append(trn_loss)
    34. # 计算评价指标
    35. trn_score = self.metric(logits, y).item()
    36. self.train_scores.append(trn_score)
    37. # 计算参数梯度
    38. self.model.backward(y)
    39. if print_grads is not None:
    40. # 打印每一层的梯度
    41. print_grads(self.model)
    42. # 更新模型参数
    43. self.optimizer.step()
    44. dev_score, dev_loss = self.evaluate(dev_set)
    45. # 如果当前指标为最优指标,保存该模型
    46. if dev_score > best_score:
    47. self.save_model(save_path)
    48. print(f"best accuracy performence has been updated: {best_score:.5f} --> {dev_score:.5f}")
    49. best_score = dev_score
    50. if epoch % log_epochs == 0:
    51. print(f"[Train] epoch: {epoch}, loss: {trn_loss}, score: {trn_score}")
    52. print(f"[Dev] epoch: {epoch}, loss: {dev_loss}, score: {dev_score}")
    53. def evaluate(self, data_set):
    54. X, y = data_set
    55. # 计算模型输出
    56. logits = self.model(X)
    57. # 计算损失函数
    58. loss = self.loss_fn(logits, y).item()
    59. self.dev_loss.append(loss)
    60. # 计算评价指标
    61. score = self.metric(logits, y).item()
    62. self.dev_scores.append(score)
    63. return score, loss
    64. def predict(self, X):
    65. return self.model(X)
    66. def save_model(self, save_path):
    67. torch.save(self.model.params, save_path)
    68. def load_model(self, model_path):
    69. self.model.params = torch.load(model_path)

    tools.py

    1. import matplotlib.pyplot as plt
    2. #新增绘制图像方法
    3. def plot(runner,fig_name):
    4. plt.figure(figsize=(10,5))
    5. plt.subplot(1,2,1)
    6. epochs = [i for i in range(len(runner.train_scores))]
    7. #绘制训练损失变化曲线
    8. plt.plot(epochs, runner.train_loss, color='#e4007f', label="Train loss")
    9. #绘制评价损失变化曲线
    10. plt.plot(epochs, runner.dev_loss, color='#f19ec2', linestyle='--', label="Dev loss")
    11. #绘制坐标轴和图例
    12. plt.ylabel("loss", fontsize='large')
    13. plt.xlabel("epoch", fontsize='large')
    14. plt.legend(loc='upper right', fontsize='x-large')
    15. plt.subplot(1,2,2)
    16. #绘制训练准确率变化曲线
    17. plt.plot(epochs, runner.train_scores, color='#e4007f', label="Train accuracy")
    18. #绘制评价准确率变化曲线
    19. plt.plot(epochs, runner.dev_scores, color='#f19ec2', linestyle='--', label="Dev accuracy")
    20. #绘制坐标轴和图例
    21. plt.ylabel("score", fontsize='large')
    22. plt.xlabel("epoch", fontsize='large')
    23. plt.legend(loc='lower right', fontsize='x-large')
    24. plt.tight_layout()
    25. plt.savefig(fig_name)
    26. plt.show()

    (PS:累太累了,下次证明少写点,嘤嘤嘤,小公式没给我累死,好处是对softmax有了更深的理解了)

  • 相关阅读:
    QT信号槽
    hive笔记(三):DDL数据定义/DML数据操作-数据库、表、数据导入导出
    安装 Windows 7 VM虚拟机
    五种方式实现 Java 单例模式
    Java算法-力扣leetcode-125. 验证回文串
    彻底删除vscode以及vscode的插件记录
    LeetCode hot100-10
    你可需要的对象存储中间件《Minio使用》
    树莓派学习笔记--串口通信(配置硬件串口进行通信)
    HIVE消费者画像
  • 原文地址:https://blog.csdn.net/m0_70026215/article/details/133690588