3 神经网络基础及应用

本节课讲述神经网络的基础知识,并利用paddle线性模型解决经典的波士顿房价问题,最后使用paddleX实现垃圾分类

1、神经网络基础

神经网络部分的基础知识参考该课程:深度学习保姆级教学

在了解神经网络之前,强烈建议同学们先补充一下机器视觉的基础知识,建议看这个教程,较为通俗易懂:黑马程序员—机器学习教程

1.1 为什么需要深度学习?

业界广泛流传:数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。所有人工智能任务最关键的并不是算法,而是数据,巧妇难为无米之炊,什么样的特征更适合当前的问题更加重要。在传统的机器学习方法中,需要人工设计和选择特征,这是一个耗时且需要专业知识的过程。而深度学习模型,尤其是卷积神经网络(CNN)和循环神经网络(RNN),具有自动从原始数据中学习特征的能力。这意味着它们可以自动发现数据中的重要模式,无需人工干预。

1.2 神经网络基础

神经网络是深度学习的基础,整个神经网络是比较庞大且复杂的,我们将其分为以下几个模块来一一学习,然后组合在一起,便于大家理解神经网络。

  • 得分函数
  • 损失函数
  • 正则化
  • 激活函数
  • 前向传播
  • 优化算法
  • 反向传播
  • 神经网络整体架构

1.2.0 图像基础概念

首先来看一下图像的基础概念: 计算机中,一张图片可被表示成三维数组的形式,例如3001003,每个像素的值从0到255,数值大小表示允许通过色彩多少,也称为灰度值,灰度越大,相应色彩越深。其中300与100是横向及纵向的像素数,3为图像的通道数,分别为R、G、B。

1.2.1 得分函数

假设现在要使用神经网络实现一个图片分类的问题,那么可以简单表示为下图这个式子f(x, w) = wx + b,输入也就是猫的图像,输出就是得到的每个类别的得分,比如猫类得3分,狗类得2分,马类得1分,哪个类别得分最多就认为输入图片属于哪个类别。

假设输入得图像大小为32×32×3,那么一共有3072个像素点,在猫的图片中,不同位置的像素点对图像分类结果的影响是一样的吗?
肯定是不一样的!
比如猫的眼睛,和没有猫的背景区域的像素点对结果的影响是不一样的,我们当然希望有用的像素点起到的正向作用更大,没用像素点起到的反向作用越小,那么通过什么实现,那就是公式里的W,也就是权重参数。

现在思考一下,对于输入的猫的这个图像,一共对应多少个权重参数?
有多少像素点就要有多少权重参数,然后还要考虑不同类别也要对应不同的权重参数,才能得到不同的结果,假设是3分类问题,那么一共是3*3072的权重参数。
除权重参数外,还有偏置参数b,权重参数起关键作用,偏置参数起着微调的作用,那么对于这个3分类任务,需要多少个偏置参数?
3个,每个类别都对应一个自己的偏置参数。

下面是假设输入图像就是四个像素得到的具体计算结果(得分值),那么请问图中这个W矩阵是怎么来的?
当然是通过优化算法迭代优化得到的,首先可以随机构建一个3*4的矩阵,在神经网络中不停进行迭代优化,最终得到一组优异的权重矩阵。

那又该用什么参数去评价当前权重参数的好坏呢?
那就是用损失函数。

1.2.2 损失函数

神经网络可以实现回归、分类等任务,不同任务相互之间的差别就在于损失函数定义不同。
损失函数有很多种,我们举一种简单的损失函数进行计算,如下图所示。其中,Sj是当前输入属于其他类别的得分值,而Syi就是当前输入属于正确类别的得分值。
式子里面+1的意思是 容忍程度Δ,为什么要有这个1?
因为假如其他类别得分值是3.15 正确类别得分值3.2,如果损失函数没有+1,那计算出来就是0,就认为该结果比较良好,但实际上,正确类别的得分只比其他类别高了一点点,正确的并不明显,所以要加一个1,保证我的正确类别的得分值至少要比错误类别的得分值高1以上,这样才说明此时的结果正确度比较高。

1.2.3 正则化

现在有一个问题,如果损失函数的值相同,那么意味着两个模型一样吗?
假设输入值为x,权重参数分别为w1、w2,不难看出,计算出来的值一致,损失值一致。

但实际上,两组权重参数对应的效果是不同的,模型A只考虑了局部的数据,而模型B更注重全局,两者的差别可描述为下图:

对于模型A这种情况,很容易出现过拟合,要解决这种过拟合的问题,就得使用正则化
正则化通过在模型的损失函数中引入额外的惩罚项,来对模型的参数进行约束,从而降低模型的复杂度。这个额外的惩罚通常与模型参数的大小或者数量相关,旨在鼓励模型学习简单的规律,而不是过度拟合训练数据。
正则化惩罚项的数学表示如下:

1.2.4 激活函数

到目前为止,我们得到的还是对于某个输入图像的得分值,跟分类任务还有差距,通常我们要得到的是输入图像是某个类别的概率值,之前我们在机器学习里面有学过一个激活函数——sigmoid函数,其中z就是得分值,通过该函数就能得到0-1之间的一个概率值

在神经网络当中也是一样,最终要得到的是一个概率值,sigmoid函数解决的是二分类问题,除了二分类,现实世界往往有其他类型的问题,比如咱们前面说的输入一个猫图像计算他是多个类别的这种情况,对于这种多分类情况,就可以使用softmax函数。
当然,深度学习的激活函数有很多,比如还有relu,双曲正切激活函数等等…咱们以softmax为例进行讲解
下图分别是softmax函数的数学表示及损失值计算:

举个例子,大家就能明白softmax的用法:依旧使用猫图像作为输入,得到各个类别得分值分别是3.2、5.1、-1.7,可以发现这几个得分值之间的差距并不高,此时使用指数函数ex映射来放大得分值之间的差异,然后再使用归一化,将放大后的得分值转换为概率值,再使用交叉熵损失函数分别计算出当前输入得到正确类别的损失函数是多少,当概率值越接近1,损失值越小,概率值越小,损失值越大。

1.2.5 前向传播

目前为止,我们已经讲解的步骤加在一起就是完整的前向传播的过程,也就是我们有输入x和权重w,怎么得到对应损失值的过程。
神经网络的目标就是去根据我们得到的损失,更新权重参数W,保证得到的W更加合适,那要实现这个目标,就涉及到反向传播的概念。

1.2.6 优化算法

前面说了神经网络的目标就是找到合适的权重参数W,用什么方法找呢,那就是通过梯度下降的优化方式,迭代计算
假设有一个线性回归函数,其目标函数就是所有样本误差和的均值

梯度下降就是沿着这个目标函数下降的方向找,最后就能找到山谷的最低点,最低点对应的w就是我们需要的权重参数,梯度下降的原理表示如下:

其中α为学习率,也就是下降的步长,α后面的式子为梯度,也就是每次下降的方向,初始的w1减去学习率与梯度的乘积就得到了新的w。每下降一次,相当于一次迭代更新计算,直到找到最低点。
常见的梯度下降及特点如下:

1.2.7 反向传播

首先我们要了解一下神经网络中单独一个神经元的结构,每个神经元由两部分组成,第一部分是输入值x1 x2和权重系数w1 w2乘积的和,第二部分(f(e))是一个激活函数(非线性函数)的输出,激活函数可以是sigmoid、relu等等, y=f(e)即为某个神经元的输出。

有了上面对神经元结构的了解和梯度下降具体函数的概念,我们就可以正式开始推导反向传播过程
假设现有一个简单的神经网络,左边是输入层,中间的叫隐藏层,右边的叫输出层
现在我们进行前向运算,计算一下损失函数的值

下面是反向传播,我们先来求最简单的,求误差E对w5的导数。首先明确这是一个“链式求导”过程,要求误差E对w5的导数,需要先求误差E对out o1的导数,再求out o1对net o1的导数,最后再求net o1对w5的导数,经过这个链式法则,我们就可以求出误差E对w5的导数(偏导),如下图所示,导数(梯度)已经计算出来了,下面就是反向传播与参数更新过程。

1.2.8 神经网络整体架构

刚才的反向传播推导没看懂也没关系,只要知道,神经网络的权重参数计算流程就是:前向传播得到损失,然后反向传播计算梯度,利用梯度更新权重参数,重复这个过程,直到得到效果较好的权重参数即可

现在我们来讲神经网络的整体架构,相当于把前面的知识全部整合起来
这张图很生动的展现了我们神经网络的基本框架,我们会发现只要理解了这张图,就掌握了神经网络的大部分内容。我们在图中不难看出整体架构包括层次结构,神经元,全连接,非线性四个部分,所以我们就依据它们来展开讲解。
1、层次结构:在输入层中的每一个神经元里面是你输入原始数据(一般称为X)的不同特征,比如x为一张图片,这张图片的像素是32323,其中的每一个像素都是它的特征吧,所以有3072个特征对应的输入层神经元个数就是3072个,这些特征以矩阵的形式进行输入的。我们举个例子比如我们的输入矩阵为‘13072’(第一维的数字表示一个batch(batch指的是每次训练输入多少个数据)中有多少个输入;第二维数字中的就是每一个输入有多少特征)
在隐藏层中的每一层神经元表示对x进行一次更新的数据,而每层有几个神经元(比如图中hidden1层中有四个神经元)表示将你的输入数据的特征扩展到几个(比如图中就是四个),就比如你的输入三个特征分别为年龄,体重,身高,而图中hidden1层中第一个神经元中经过变换可以变成这样‘年龄
0.1+体重0.4+身高0.5’,而第二个神经元可以表示成‘年龄0.2+体重0.5+身高0.3’,每一层中的神经元都可以有不同的表示形式。
在输出层中的的神经元个数主要取决于你想要让神经网络干什么,比如你想让它做一个10分类问题,输出层的矩阵就可以是’1
10’的矩阵(第一维表示的与输入层表示数字相同,后面10就是10种分类)。
2、全连接
我们看到的每一层和下一层中间都有灰色的线,这些线就被称为全连接(因为你看上一层中每个神经元都连接着下一层中的所有神经元),而这些线我们也可以用一个矩阵表示,这个矩阵我们通常称为‘权重矩阵’,用大写的W来表示(是后续我们需要更新的参数)。权重矩阵W的维数主要靠的是上一层进来数据的输入数据维数和下一层需要输入的维数,可以简单理解为上有一层有几个神经元和下一层有几个神经元,例如图中input layer中有3个神经元,而hidden1 layer中有4个神经元,中的W的维度就为‘3*4’,以此类推
3、非线性
在每层运算做完后,我们得数据不能直接输入到下一层计算中,需要添加一些非线性函数(大部分也可以叫做激活函数),常用的激活函数有relu,sigmoid,tanh(读者可以去查查看),就比如说在input layer 在hidden1 layer计算完后不能将数据直接传如hidden2 layer在这之间需要添加一个激活函数。

2、完整深度学习实现流程

完整的深度学习实现流程如下:

具体的流程讲解,可见博客:深度学习的基本实现流程

3、线性模型实例——波士顿房价预测

在实现波士顿房价预测之前,我们先通过一个helloworld级别的paddle实例,带大家熟悉一下paddle的用法

假设现在有一个任务: 乘坐出租车的时候,会有一个10元的起步价,只要上车就需要收取。出租车每行驶1公里,需要再支付每公里2元的行驶费用。当一个乘客坐完出租车之后,车上的计价器需要算出来该乘客需要支付的乘车费用
上面的过程,用python实现如下:

def calculate_fee(distance_travelled):
    return 10 + 2 * distance_travelled

for x in [1.0, 3.0, 5.0, 9.0, 10.0, 20.0]:
    print(calculate_fee(x))

输出结果为:

12.0
16.0
20.0
28.0
30.0
50.0

现在对问题进行改变:
现在知道乘客每次乘坐出租车的公里数,也知道乘客每次下车的时候支付给出租车司机的总费用。但是并不知道乘车的起步价,以及每公里行驶费用是多少。希望让机器从这些数据当中学习出来计算总费用的规则
假设总费用为total_fee,行驶里程为distance_travelled,设w为每公里的费用,b为起步价,则计算总费用的规则表示如下:
total_fee = w * distance_travelled + b
则完整的实现代码如下,这是一个明显的线性问题,因此使用线性模型实现:

# step1: 导入飞浆
import paddle
print("paddle " + paddle.__version__)

# step2: 准备数据
# distance_travelled即x,特征feature   total_fee即y,标签Label
# 用paddle.to_tensor把示例数据转换为paddle的Tensor数据
x_data = paddle.to_tensor([[1.], [3.0], [5.0], [9.0], [10.0], [20.0]])
y_data = paddle.to_tensor([[12.], [16.0], [20.0], [28.0], [30.0], [50.0]])
print(x_data)
print(y_data)

# step3: 定义计算模型
# y_predict = w * x + b   使用飞桨的线性变换层:paddle.nn.Linear实现计算过程
# in_features:该层的输入特征数量    out_features:该层的输出特征数量
linear = paddle.nn.Linear(in_features=1, out_features=1)

# step4: 参数初始化
# 机器在一开始的时候会随便猜w和b  w是一个随机值,b是0.0(飞桨的初始化策略)
# .weight:访问PaddlePaddle中线性层权重参数  .numpy():将PaddlePaddle的Tensor对象转换为一个NumPy数组  .item():将一个形状为 () 的NumPy数组转换为该元素本身
# 从线性层 linear 中获取 weight 属性,将其作为一个NumPy数组来处理,然后提取出这个数组中的单个标量值,将其赋值给变量 w_before_opt
w_before_opt = linear.weight.numpy().item()
b_before_opt = linear.bias.numpy().item()

print("w before optimize: {}".format(w_before_opt))
print("b before optimize: {}".format(b_before_opt))

# step5:计算w b
# 用最简单的均方误差(mean square error)作为损失函数(paddle.nn.MSELoss)
# 最常见的优化算法SGD(stocastic gradient descent)作为优化算法 通过调整网络权重来最小化损失函数
mse_loss = paddle.nn.MSELoss()
# learning_rate:学习率(超参数),决定每次参数更新时步长的大小     parameters:指定了优化器需要更新的参数,这里是linear.parameters(),即线性层 linear 中所有的参数w b
sgd_optimizer = paddle.optimizer.SGD(learning_rate=0.001, parameters = linear.parameters())

# 在机器学习/深度学习当中,机器(计算机)在最开始的时候,得到参数w和b的方式是随便猜一下,用这种随便猜测得到的参数值,
# 去进行计算(预测)的时候,得到的y_predict,跟实际的y值一定是有差距的。接下来,机器会根据这个差距来调整w和b,
# 随着这样的逐步的调整,w和b会越来越正确,y_predict跟y之间的差距也会越来越小,从而最终能得到好用的w和b。
# 这个过程就是机器学习的过程

# step6:开始训练
# 训练轮次
total_epoch = 5000
for i in range(total_epoch):
    # 前向传播 计算预测值(计算损失)
    y_predict = linear(x_data)
    loss = mse_loss(y_predict, y_data)
    # 反向传播 求梯度
    loss.backward()
    # 优化器走一个步长
    sgd_optimizer.step()
    # 清空当前梯度
    sgd_optimizer.clear_grad()
    # 每一千轮 打印一次损失值
    if i % 1000 == 0:
        print("epoch {} loss {}".format(i, loss.numpy()))

# 打印最终的损失值
print("finished training, loss {}".format(loss.numpy()))

输出结果为:

w before optimize: 0.46680963039398193
b before optimize: 0.0
epoch 0 loss 586.6461791992188
epoch 1000 loss 8.120820045471191
epoch 2000 loss 1.8157562017440796
epoch 3000 loss 0.4059947729110718
epoch 4000 loss 0.09077949076890945
finished training, loss 0.02032865211367607

波士顿房价预测是非常经典的线性回归问题,假设uci-housing数据集中的房子属性和房价之间的关系可以被属性间的线性组合描述。在模型训练阶段,让假设的预测结果和真实值之间的误差越来越小。在模型预测阶段,预测器会读取训练好的模型,对从未遇见过的房子属性进行房价预测。

下面给出完整的代码:

# 导入需要用到的package
import numpy as np  # 导入NumPy库,用于进行数学运算
import paddle  # 导入PaddlePaddle库,用于深度学习模型的构建和训练
import paddle.nn.functional as F  # 导入PaddlePaddle的函数库,用于模型构建

# 封装数据处理
def load_data():
    # 从文件导入数据
    datafile = './housing.data'  # 数据文件的路径
    data = np.fromfile(datafile, sep=' ', dtype=np.float32)  # 读取数据,以空格为分隔,数据类型为浮点数

    # 每条数据包括14项,其中前面13项是影响因素,第14项是相应的房屋价格中位数
    feature_names = [ 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', \
                      'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT', 'MEDV' ]
    feature_num = len(feature_names)  # 特征的数量

    # 将原始数据进行Reshape,变成[N, 14]这样的形状
    # data.shape[0]: data 数组的第一维度的大小,即数据点的总数 N     //:Python 中的整数除法运算符,返回除法操作的整数结果
    # 原始的一维数据数组被重新组织成一个二维数组,其中每一行代表一个数据点,每一列代表一个特征
    data = data.reshape([data.shape[0] // feature_num, feature_num])
    # 将原数据集拆分成训练集和测试集
    # 这里使用80%的数据做训练,20%的数据做测试
    ratio = 0.8  # 训练集占总数据的比例
    offset = int(data.shape[0] * ratio)  # 计算训练集的行数
    training_data = data[:offset]  # 获取训练集 0~offset 左闭右开

    # 计算训练集的最大值,最小值和平均值
    # axis=0 表示沿着列的方向计算最大值,即对于每一列(也就是每个特征),找到最大的元素
    maximums, minimums, avgs = training_data.max(axis=0), training_data.min(axis=0), training_data.sum(axis=0) / training_data.shape[0]

    # 记录数据的归一化参数,在预测时对数据做归一化
    global max_values  # 使用global声明全局变量,用于存储最大值
    global min_values  # 使用global声明全局变量,用于存储最小值
    global avg_values  # 使用global声明全局变量,用于存储平均值
    max_values = maximums
    min_values = minimums
    avg_values = avgs

    # 对数据进行归一化处理
    for i in range(feature_num):
        # data[:, i]:第i个特征的所有数据点
        data[:, i] = (data[:, i] - avgs[i]) / (maximums[i] - minimums[i])

    # 训练集和测试集的划分比例
    training_data = data[:offset]  # 划分训练集
    test_data = data[offset:]  # 划分测试集
    return training_data, test_data  # 返回训练集和测试集

# 定义一个回归器
class Regressor(paddle.nn.Layer):  # 继承自PaddlePaddle的Layer类   paddle.nn.Layer 是 PaddlePaddle 提供的基类,用于构建神经网络模型
    # self代表类的实例自身
    # __init__类的构造函数(初始化方法),它在创建类的新实例时被调用
    def __init__(self):
        # 调用了基类 paddle.nn.Layer 的构造函数,初始化父类中的一些参数
        super().__init__()
        # 定义一层全连接层,输入维度是13,输出维度是1
        self.fc = paddle.nn.Linear(13, 1)

    # 网络的前向计算 得到预测值
    def forward(self, inputs):
        pred = self.fc(inputs)  # 计算全连接层的输出
        return pred

# 声明定义好的线性回归模型
model = Regressor()
# 开启模型训练模式
model.train()
# 加载数据
training_data, test_data = load_data()
# 定义优化算法,使用随机梯度下降SGD
# 学习率设置为0.01
opt = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters())

# 设置外层循环次数 训练轮次
EPOCH_NUM = 10
# 设置batch大小 每个批次的样本数
BATCH_SIZE = 10

# 定义外层循环
for epoch_id in range(EPOCH_NUM):
    # 在每轮迭代开始之前,将训练数据的顺序随机打乱 模型学习到数据的一般性特征,而不是仅仅记住训练数据的特定顺序。
    np.random.shuffle(training_data)
    # 将训练数据进行拆分,mini_batches(包含训练集所有数据)列表每个元素是10条数据 0~9  10~11 ....
    mini_batches = [training_data[k:k + BATCH_SIZE] for k in range(0, len(training_data), BATCH_SIZE)]
    # 定义内层循环
    # 为 mini_batches 列表中的每个元素生成一个包含索引和元素值的元组  iter_id是当前小批量的索引 mini_batch即为10条数据
    for iter_id, mini_batch in enumerate(mini_batches):
        x = np.array(mini_batch[:, :-1])  # 获取当前批次训练数据的特征
        y = np.array(mini_batch[:, -1])  # 获取当前次批训练标签(真实房价)
        # 将numpy数据转为飞浆动态图tensor形式
        house_features = paddle.to_tensor(x)
        prices = paddle.to_tensor(y)

        # 前向计算 计算预测值
        predicts = model(house_features)

        # 计算损失
        loss = F.square_error_cost(predicts, label=prices)  # 计算平方误差损失
        avg_loss = paddle.mean(loss)  # 计算损失的平均值
        # 200条数据打印一次
        if iter_id % 20 == 0:
            print("epoch: {}, iter: {}, loss is: {}".format(epoch_id, iter_id, avg_loss.numpy()))

        # 反向传播
        avg_loss.backward()
        # 最小化loss, 更新参数
        opt.step()
        # 清除梯度
        opt.clear_grad()

# 保存模型参数,文件名为LR_model.pdparams
paddle.save(model.state_dict(), 'LR_model.pdparams')
print("模型保存成功,模型参数保存在LR_model.pdparams中")

def load_one_example():
    # 从上边已加载的测试集中,随机选择一条作为测试数据
    idx = np.random.randint(0, test_data.shape[0])
    # one_data:包含 test_data 第 idx 行的所有列,除了最后一列(由 [:-1] 表示)
    one_data, lable = test_data[idx, :-1], test_data[idx, -1]
    # 修改该条数据shape为[1,13]
    one_data = one_data.reshape([1, -1])

    return one_data, lable

# 参数为保存模型参数的文件地址
model_dict = paddle.load('LR_model.pdparams')
model.load_dict(model_dict)
model.eval()  # 设置模型为评估模式

# 参数为数据集的文件地址
one_data, lable = load_one_example()
# 将数据转为动态图的variable格式
one_data = paddle.to_tensor(one_data)
predict = model(one_data)

# 对结果做反归一化处理
predict = predict * (max_values[-1] - min_values[-1]) + avg_values[-1]
# 对label数据做反归一化处理
label = lable * (max_values[-1] - min_values[-1]) + avg_values[-1]

print("Inference result is {},the corresponding label is {}".format(predict.numpy(), label))

输出结果为:

epoch: 0, iter: 0, loss is: 0.06825575977563858
epoch: 0, iter: 20, loss is: 0.05843830108642578
epoch: 0, iter: 40, loss is: 0.05767223238945007
epoch: 1, iter: 0, loss is: 0.09777715057134628
epoch: 1, iter: 20, loss is: 0.18931254744529724
epoch: 1, iter: 40, loss is: 0.1781318187713623
epoch: 2, iter: 0, loss is: 0.06990115344524384
epoch: 2, iter: 20, loss is: 0.07100893557071686
epoch: 2, iter: 40, loss is: 0.1746169924736023
epoch: 3, iter: 0, loss is: 0.018296070396900177
epoch: 3, iter: 20, loss is: 0.06697437912225723
epoch: 3, iter: 40, loss is: 0.08777721971273422
epoch: 4, iter: 0, loss is: 0.048440709710121155
epoch: 4, iter: 20, loss is: 0.07055813819169998
epoch: 4, iter: 40, loss is: 0.04246256873011589
epoch: 5, iter: 0, loss is: 0.07400884479284286
epoch: 5, iter: 20, loss is: 0.11619622260332108
epoch: 5, iter: 40, loss is: 0.09324172139167786
epoch: 6, iter: 0, loss is: 0.038065068423748016
epoch: 6, iter: 20, loss is: 0.08827249705791473
epoch: 6, iter: 40, loss is: 0.09569618105888367
epoch: 7, iter: 0, loss is: 0.03524096682667732
epoch: 7, iter: 20, loss is: 0.05658217892050743
epoch: 7, iter: 40, loss is: 0.07819092273712158
epoch: 8, iter: 0, loss is: 0.0899442732334137
epoch: 8, iter: 20, loss is: 0.07805903255939484
epoch: 8, iter: 40, loss is: 0.033808447420597076
epoch: 9, iter: 0, loss is: 0.09970776736736298
epoch: 9, iter: 20, loss is: 0.035955317318439484
epoch: 9, iter: 40, loss is: 0.03546943515539169
模型保存成功,模型参数保存在LR_model.pdparams中
Inference result is [[16.49474]],the corresponding label is 14.5

4、paddleX实现垃圾分类

参考教程:paddle计算机视觉入门教程

4.1 环境搭建

可以参考:搭建并配置Paddlex的推理环境
由于python版本、paddle版本等各种问题,原先搭建的paddle虚拟开发环境并不能顺利的安装paddleX,因此强烈建议大家使用anaconda新建一个虚拟环境

假设大家已经安装好了visual studio,已有Microsoft Visual C++ 环境(若没有则参考上面的paddleX环境搭建博客进行安装)
没有该环境的话,在安装paddleX会出现错误,所以大家务必先保证Microsoft Visual C++ 环境已有

现在开始paddlex环境的搭建
首先,新建一个名为paddle_new_env的虚拟环境

conda create -n paddle_new_env python=3.8

python选择3.8版本,经验证,该python版本可顺利安装paddleX.
进入刚创建的虚拟环境:

conda activate paddle_new_env

安装paddle,根据自己电脑情况可以安装cpu版本的paddle,也可以安装gpu版本
当前,需要先设置对应镜像

conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --set show_channel_urls yes

# 安装cpu版本paddle
conda install paddlepaddle==2.4.2 --channel https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/Paddle/

# 安装gpu版本paddle 取决于你的cuda版本,具体参考:https://www.paddlepaddle.org.cn/documentation/docs/zh/install/conda/windows-conda.html#id1
conda install paddlepaddle-gpu==2.4.2 cudatoolkit=11.6 -c https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/Paddle/ -c conda-forge

注意不要按照paddle官网,安装paddle2.6.1,要安装2.4.2版本的Paddle,否则paddle无法正常使用
安装好paddle后,进行paddleX的安装,执行如下指令:

pip install paddlex -i https://mirror.baidu.com/pypi/simple

安装成功后,输入conda list,出现如下信息,说明已经安装上

输入python进入Python解释器,输入:

import paddlex
print(paddlex.__version__)

此时出现报错:

>>> import paddlex
F:\anaconda\envs\paddle_env_new\lib\site-packages\setuptools\sandbox.py:14: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html 
  import pkg_resources
F:\anaconda\envs\paddle_env_new\lib\site-packages\pkg_resources\__init__.py:2832: DeprecationWarning: Deprecated call to `pkg_resources.declare_namespace('mpl_toolkits')`.
Implementing implicit namespace packages (as specified in PEP 420) is preferred to `pkg_resources.declare_namespace`. See https://setuptools.pypa.io/en/latest/references/keywords.html#keyword-namespace-packages 
  declare_namespace(pkg)
F:\anaconda\envs\paddle_env_new\lib\site-packages\pkg_resources\__init__.py:2832: DeprecationWarning: Deprecated call to `pkg_resources.declare_namespace('google')`.
Implementing implicit namespace packages (as specified in PEP 420) is preferred to `pkg_resources.declare_namespace`. See https://setuptools.pypa.io/en/latest/references/keywords.html#keyword-namespace-packages 
  declare_namespace(pkg)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "F:\anaconda\envs\paddle_env_new\lib\site-packages\paddlex\__init__.py", line 17, in <module>
    from paddlex.utils.env import get_environ_info, init_parallel_env
  File "F:\anaconda\envs\paddle_env_new\lib\site-packages\paddlex\utils\__init__.py", line 20, in <module>
    from .checkpoint import get_pretrain_weights, load_pretrain_weights, load_checkpoint
  File "F:\anaconda\envs\paddle_env_new\lib\site-packages\paddlex\utils\checkpoint.py", line 21, in <module>
    from .download import download_and_decompress
  File "F:\anaconda\envs\paddle_env_new\lib\site-packages\paddlex\utils\download.py", line 24, in <module>
    import filelock
ModuleNotFoundError: No module named 'filelock'

该错误表明在尝试导入PaddleX时,Python环境中缺少名为filelock的模块。filelock是一个用于文件锁定的库,通常用于确保在多线程或多进程环境中文件被安全地访问。

# 使用conda安装filelock
conda install -c conda-forge filelock

如果出现下面错误:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "F:\anaconda\envs\pp_env_new\lib\site-packages\paddlex\__init__.py", line 17, in <module>
    from paddlex.utils.env import get_environ_info, init_parallel_env
  File "F:\anaconda\envs\pp_env_new\lib\site-packages\paddlex\utils\__init__.py", line 15, in <module>
    from . import logging
  File "F:\anaconda\envs\pp_env_new\lib\site-packages\paddlex\utils\logging.py", line 21, in <module>
    import paddle
  File "F:\anaconda\envs\pp_env_new\lib\site-packages\paddle\__init__.py", line 25, in <module>
    from .framework import monkey_patch_variable
  File "F:\anaconda\envs\pp_env_new\lib\site-packages\paddle\framework\__init__.py", line 17, in <module>
    from . import random  # noqa: F401
  File "F:\anaconda\envs\pp_env_new\lib\site-packages\paddle\framework\random.py", line 16, in <module>
    import paddle.fluid as fluid
  File "F:\anaconda\envs\pp_env_new\lib\site-packages\paddle\fluid\__init__.py", line 36, in <module>
    from . import framework
  File "F:\anaconda\envs\pp_env_new\lib\site-packages\paddle\fluid\framework.py", line 35, in <module>
    from .proto import framework_pb2
  File "F:\anaconda\envs\pp_env_new\lib\site-packages\paddle\fluid\proto\framework_pb2.py", line 33, in <module>
    _descriptor.EnumValueDescriptor(
  File "F:\anaconda\envs\pp_env_new\lib\site-packages\google\protobuf\descriptor.py", line 914, in __new__
    _message.Message._CheckCalledFromGeneratedFile()
TypeError: Descriptors cannot be created directly.
If this call came from a _pb2.py file, your generated code is out of date and must be regenerated with protoc >= 3.19.0.
If you cannot immediately regenerate your protos, some other possible workarounds are:
 1. Downgrade the protobuf package to 3.20.x or lower.
 2. Set PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python (but this will use pure-Python parsing and will be much slower).

More information: https://developers.google.com/protocol-buffers/docs/news/2022-05-06#python-updates

解决方法是:

pip install "protobuf==3.20.0"

切记不能将protobuf版本调整到3.20.0以下,不然会出现如下错误,这个问题应该是protobuf 包的版本与 VisualDL 或 PaddleX 不兼容导致的,所以要保证protobuf改到3.20.0版本即可
在这里插入图片描述
当然,也可以设置环境变量,作为临时解决方案,但是下次使用该虚拟环境还是会报错

set PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python

重新执行:

import paddlex
print(paddlex.__version__)

当输出对应PaddleX版本时,则说明安装成功
在这里插入图片描述
打开pycharm, 添加新的解释器,选择新建paddle_new_env的,即可使用安装好的paddleX。
1、添加新的解释器
在这里插入图片描述
2、点击Conda环境,选择conda可执行文件,选择刚新建的虚拟环境,点击确定

3、观察pycharm右下角,发现已经切换到了刚才的虚拟环境
在这里插入图片描述

4.2 垃圾分类实战

4.2.1 数据集准备

数据集下载链接:
链接:https://pan.baidu.com/s/1ZSHQft4eIpYHliKRxZcChQ
提取码:hce7

本次实战为图片分类任务,数据集结构如下:
在这里插入图片描述
分别为分类的图片文件夹,建议有一个类别就建一个文件夹,方便管理。每个文件夹里面放尽量多的图片,图片种类尽量多样、随机。训练集和评价集的标签文件,格式如下:
./3/933.jpg 3
./2/1670.jpg 2
./2/2175.jpg 2
./1/934.jpg 1
./1/1653.jpg 1

前面为图片的相对路径,后面为对应的标签类别。训练集用于模型每一轮的训练,评价集用于测试每一轮训练的结果的精度,即取对应的图片,使用当前轮次训练出来的模型计算出对应结果,并与评价集中的真实结果进行对比,从而得到当前模型的精确度。
labels.txt存放对应的标签,格式如下:

有害垃圾
可回收垃圾
厨房垃圾
其他垃圾

提供文件重命名的代码,用于每个文件夹里面图片的重命名,将原本没有顺序的一大堆图片文件,重命名为1.* 2.* 3.* …

# 对文件夹里面的文件进行改名,变成1.*  2.*  3.* .......
import os  # 导入os模块,它提供了与操作系统交互的功能

def rename():  # 定义一个名为rename的函数
    res = os.listdir('./')  # 列出当前目录下的所有文件和文件夹,并将它们存储在res列表中
    print(res)
    for a in res:  # 遍历res列表中的每个项目
        i = 0  # 初始化一个计数器i,用于为文件命名
        flag = os.path.isdir(a)  # 检查当前项目是否是文件夹,返回布尔值,存储在变量flag中
        if(flag == False):  # 如果flag为False,即当前项目不是文件夹,则跳过当前循环的剩余部分
            continue  # continue语句会跳过当前循环的剩余迭代,直接开始下一次迭代
        path = a  # 如果当前项目是文件夹,将其路径赋值给变量path
        filelist = os.listdir(path)  # 列出path指向的文件夹下所有的文件和文件夹,包括子文件夹,存储在filelist列表中

        for files in filelist:  # 遍历filelist中的每个项目
            i = i + 1  # 每次遍历文件时,增加计数器i
            Olddir = os.path.join(path, files)  # 拼接旧的文件路径,包括文件夹路径和文件名
            if os.path.isdir(Olddir):  # 如果Olddir是一个文件夹,则跳过
                continue  # 继续下一次迭代
            filename = os.path.splitext(files)[0]  # 使用os.path.splitext方法分离文件名和扩展名,取文件名部分
            filetype = os.path.splitext(files)[1]  # 取文件扩展名部分
            Newdir = os.path.join(path, str(i) + filetype)  # 创建新的文件路径,将计数器i转换为字符串,与文件扩展名拼接
            os.rename(Olddir, Newdir)  # 使用os.rename方法重命名文件,将旧路径重命名为新路径
rename()  # 调用rename函数执行上述操作

提供生成train.txt和eval.txt文件的脚本代码,分类的比例为5:1,train.txt与eval.txt中的数据都进行打乱,保证数据集、验证集有更好的随机性。

import os  # 导入os模块,提供与操作系统交互的功能
import random  # 导入random模块,用于生成随机数和打乱列表顺序

def ReadFileDatas():  # 定义一个函数用于读取文件数据
    FileNamelist = []  # 初始化一个空列表,用于存储文件名
    file = open('train.txt','r+')  # 打开文件train.txt进行读取
    for line in file:  # 遍历文件中的每一行
        line = line.strip('\n')  # 删除每行末尾的换行符\n
        FileNamelist.append(line)  # 将处理后的行添加到文件名列表中
    file.close()  # 关闭文件
    return FileNamelist  # 返回文件名列表

def WriteDatasToFile(listInfo):  # 定义一个函数用于将数据写入文件
    file_handle_train = open('train.txt', mode='w')  # 打开或创建train.txt文件用于写入
    file_handle_eval = open("eval.txt", mode='w')  # 打开或创建eval.txt文件用于写入
    i = 0  # 初始化计数器i
    for idx in range(len(listInfo)):  # 遍历列表中的每个元素
        str = listInfo[idx]  # 获取当前元素
        str_Result = str + '\n'  # 创建新的字符串,将当前字符串和换行符拼接
        if (i % 6 != 0):  # 如果计数器i除以6的余数不为0
            file_handle_train.write(str_Result)  # 将新字符串写入train.txt
        else:  # 否则
            file_handle_eval.write(str_Result)  # 将新字符串写入eval.txt
        i += 1  # 增加计数器i
    file_handle_train.close()  # 关闭train.txt文件
    file_handle_eval.close()  # 关闭eval.txt文件

path = './'  # 设置文件夹路径为当前目录
res = os.listdir(path)  # 列出当前目录下的所有文件和文件夹,并将它们存储在res列表中
print(res)  # 打印res列表
with open("train.txt", "w") as f:  # 使用with语句打开train.txt文件用于写入
    for i in res:  # 遍历res列表中的每个元素
        if (os.path.isdir(i)):  # 如果当前元素是一个文件夹
            path1 = path + i  # 获取文件夹的完整路径
            res2 = os.listdir(path1)  # 列出该文件夹下的所有文件和文件夹
            for j in res2:  # 遍历该文件夹下的所有文件
                f.write(path1 + "/" + j + " " + i + '\n')  # 将文件路径和文件夹名写入train.txt,并追加换行符

listFileInfo = ReadFileDatas()  # 调用ReadFileDatas函数读取train.txt文件数据到listFileInfo列表

# 打乱列表中的顺序
random.shuffle(listFileInfo)  # 使用random模块的shuffle函数打乱listFileInfo列表的顺序
WriteDatasToFile(listFileInfo)  # 调用WriteDatasToFile函数将打乱顺序后的listFileInfo列表数据写入train.txt和eval.txt文件

4.2.2 模型训练

助于PaddleX,模型训练变得非常简单,主要分为数据集定义数据增强算子定义模型定义模型训练四个步骤,代码有较为详细的注释,更多信息可以查看paddleX对应的api说明:PaddleX API说明

# 垃圾分类的训练文件   paddleX的api接口说明:https://paddlex.readthedocs.io/zh-cn/release-1.3/apis/datasets.html#paddlex-datasets-imagenet
from paddlex import transforms as T
import paddlex as pdx

# 定义训练数据的变换操作,这些操作将被应用到训练数据集中的每个样本上
# train_transforms:训练集的数据增强算子
train_transforms = T.Compose([
    T.RandomCrop(crop_size=224),  # 随机裁剪图像到 224x224 像素
    T.RandomHorizontalFlip(),      # 随机水平翻转图像
    T.Normalize()                 # 标准化图像,通常包括减去均值和除以标准差
])

# 定义评估数据的变换操作,这些操作将被应用到评估数据集中的每个样本上
# eval_transforms:评估集的数据增强算子
eval_transforms = T.Compose([
    T.ResizeByShort(short_size=256),  # 根据图像的短边进行缩放,确保短边为 256 像素
    T.CenterCrop(crop_size=224),      # 从图像中心裁剪出 224x224 像素的区域
    T.Normalize()                       # 标准化图像
])

# 加载训练数据集,PaddleX 提供了多种数据集加载方式,这里使用的是 ImageNet (分类数据集)格式
train_dataset = pdx.datasets.ImageNet(
    data_dir='rubbish',               # 数据集所在的文件夹
    file_list='rubbish/train.txt',   # 包含训练图像路径和标签的文件
    label_list='rubbish/labels.txt',  # 包含类别名称的文件(标签文件)
    transforms=train_transforms,      # 应用到训练数据的变换操作(训练集的增强算子)
    shuffle=True                     # 在每个epoch开始时随机打乱数据(这里也提供了打乱操作)
)

# 加载评估数据集,格式与训练数据集类似,但通常不进行随机打乱
eval_dataset = pdx.datasets.ImageNet(
    data_dir='rubbish',
    file_list='rubbish/eval.txt',
    label_list='rubbish/labels.txt',
    transforms=eval_transforms
)

# 获取类别数量,即标签的数量
num_classes = len(train_dataset.labels)

# 创建一个模型实例,这里使用的是 MobileNetV3_small 模型,它是一个轻量级的深度学习模型,适合图像分类任务(后面会细讲)
model = pdx.cls.MobileNetV3_small(num_classes=num_classes)

# 训练模型
# num_epochs=1               # 训练的总轮数
# train_dataset               # 训练数据集
# train_batch_size=64         # 每个训练批次的样本数量 如果使用GPU训练,该值取决于显存大小,越大batch_size可以设置的越大,加速训练过程,精度也会有所提高
# eval_dataset                # 评估数据集,用于在训练过程中进行性能评估
# lr_decay_epochs=[4, 6, 8]   # 学习率衰减的轮数 前期学习率大有利于收敛,后期学习率小有利于提高精度,衰减策略有很多,这里使用直接衰减(还有cos衰减等)
# save_dir='output/mobilenetv3_small'  # 保存模型的目录
# use_vdl=True               # 是否使用 VisualDL 进行可视化训练
model.train(
    num_epochs=1,
    train_dataset=train_dataset,
    train_batch_size=64,
    eval_dataset=eval_dataset,
    lr_decay_epochs=[4, 6, 8],
    save_dir='output/mobilenetv3_small',
    use_vdl=True
)

训练结束,得到对应output文件夹:
在这里插入图片描述
其中,best_model是我们经过多轮训练得到的最好的模型(这里演示只训练了一轮也就是第一轮的模型);pretrain是预训练模型,在训练前从网上下载,目的是提高我们的训练精度
打开best_model文件夹如下:
在这里插入图片描述
其中,model.yml是模型的标注文件,可提供模型的相关信息。

4.2.3 模型评估

import paddlex as pdx
model = pdx.load_model('output/mobilenetv3_small/best_model') # 加载保存的最优模型
result = model.predict('188.jpg')  # 使用模型进行预测
print("Predict Result: ", result)

可以观察输出的结果是否正确。

4.2.4 模型训练的可视化

visualdl --logdir output/mobilenetv3_small --port 8001

打开浏览器输出网址,可以看到训练的各个参数曲线。
可以在pycharm打开终端,进入对应的目录下,输入上述命令,得到一个网址,打开即可
在这里插入图片描述
打开后的结果为,可以查看对应的损失值、精确度等在训练过程中的变化情况

4.2.5 飞浆线上环境完成模型训练

大部分同学的电脑可能没有英伟达的独显,或者显卡性能较弱,这时候我们可以使用飞浆的线上资源来进行模型的训练,这一部分留给同学们自己进行尝试。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值