TensorFlow2笔记

第一部分

神经网络设计过程

与TensorFlow1差不多。

只是函数不同。

代码p13.

张量(Tensor)

讲解了创建Tensor的方法:

tf.constan(张量内容,dtype=数据类型(可选))

方法即可创建。

在这里插入图片描述

在这里插入图片描述

numpy数据类型转换为Tensor数据类型:

tf.convenrt_to_tensor(数据名,dtype=数据类型(可选))

在这里插入图片描述

生成正态分布的随机数

默认均值为0,标准差为1

tf.random.normal(维度,mean=均值,stddev=标准差)

在这里插入图片描述

生成截断式正态分布的随机数:

tf.random.truncated_normal(维度,mean=均值,stddev=标准差)

在这里插入图片描述

生成均匀分布随机数:

tf.random.uniform(维度,minval=最小值,maxval=最大值)

在这里插入图片描述

代码:p17、p18、p21、p22

常用函数

看截图课件

tf.Variable()函数:将变量标记为“可训练”,被标记的变量会在反向传播中记录梯度信息。神经网络训练中,常用该函数标记特定训练参数。

tf.data.Datset.from_tensor_slices(),切分传入张量的第一维度,生成输入特征/标签对,构建数据集。

tf.GradientTape(),使用with结构记录计算过程,gradient求出张量的梯度。

with tf.GradientTape() as tape:  # with结构到grads框起了梯度的计算过程。
    loss = tf.square(w + 1)
grads = tape.gradient(loss, w)  # .gradient函数告知谁对谁求导

tf.nn.softmax(),当n分类的n个输出(y0,y1,…,yn-1)通过softmax()函数,便符合概率分布了。使输出符合概率分布。

assign_sub(w要自减的内容)赋值操作,更新参数的值并返回。调用assign_sub()前,先用tf.Variable定义变量w为可训练(可自更新)

x = tf.Variable(4)
x.assign_sub(1)
print("x:", x)  # 4-1=3

tf.argmax(张量名,axis=操作轴),返回张量沿指定维度最大值的索引

test = np.array([[1, 2, 3], [2, 3, 4], [5, 4, 3], [8, 7, 2]])
print("test:\n", test)
print("每一列的最大值的索引:", tf.argmax(test, axis=0))  # 返回每一列最大值的索引
print("每一行的最大值的索引", tf.argmax(test, axis=1))  # 返回每一行最大值的索引

tf.cast(张量名,dtype=数据类型),强制tensor转换为该数据类型

在这里插入图片描述
在这里插入图片描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

GG

在这里插入图片描述

鸢尾花数据集读入

数据介绍:共有数据150组,每组包括花萼常、花萼宽、花瓣常、花瓣宽4个输入特征。同时给出了,这一组特征对应的莺尾花类别。类别包括Setosa lris(狗尾草莺尾)Versicolour lris(杂色莺尾),Virginica lris(弗吉尼亚莺尾)三类,分别用0,1,2表示。

步骤:

从sklearn包datasets读入数据集,语法:

from sklearn import datasets
from pandas import DataFrame
import pandas as pd

x_data = datasets.load_iris().data  # .data返回iris数据集所有输入特征
y_data = datasets.load_iris().target  # .target返回iris数据集所有标签

结构:

  花萼长度  花萼宽度  花瓣长度  花瓣宽度  类别

0 5.1 3.5 1.4 0.2 0
1 4.9 3.0 1.4 0.2 0
2 4.7 3.2 1.3 0.2 0
3 4.6 3.1 1.5 0.2 0
4 5.0 3.6 1.4 0.2 0
… … … … … …
145 6.7 3.0 5.2 2.3 2
146 6.3 2.5 5.0 1.9 2
147 6.5 3.0 5.2 2.0 2
148 6.2 3.4 5.4 2.3 2
149 5.9 3.0 5.1 1.8 2

神经网络实现鸢尾花分类

  • 准备数据
    • 数据集读入
    • 数据集乱序
    • 生成训练集和测试集
    • 配成对(输入特征,标签),每次读入一小撮
  • 搭建网络
    • 定义神经网络中所有可训练参数
  • 参数优化
    • 嵌套循环迭代,with结构更新参数,显示当前loss
  • 测试效果
    • 计算当前参数前向传播后的准确率,显示当前acc
  • acc/loss可视化

代码p45

第二部分

本节目标:学会神经网络优化过程,使用正则化减少过拟合,使用优化器更新网络参数。

预备知识

tf.where(条件语句,A,B),条件语句真返回A,条件语句假返回B。若a>b,返回a对应位置的元素,否则返回b对应位置的元素。

在这里插入图片描述

np.random.RandonState.rand(),返回一个[0,1)之间的随机数。

在这里插入图片描述

np.vstack(数组1,数组2),将两个数组按垂直方向叠加,结果:[数组1,数组2]

在这里插入图片描述

np.mgrid[起始值:结束值:步长,起始值:结束值:步长,…]

x.ravel(),将x变为一维数组,把.前变量拉直

np.c_[数组1,数组2,…]使返回的间隔数值点配对

在这里插入图片描述

p5~p7

神经网络复杂度

复杂度就是神经网络层数和神经网络参数的个数表示,看课件

在这里插入图片描述

指数衰减学习率

学习率也就是学习的步长,指数衰减学习率就是首先学习率设置的大,但随着迭代次数的增加,学习率会慢慢下降。

在这里插入图片描述

在这里插入图片描述

p10代码

激活函数

sigmoid函数,Tanh函数,Relu函数,Leaky Relu函数

经常用Relu,具体优缺点看课件

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

损失函数

均方误差、自定义损失函数、交叉熵损失函数CE、softmax与交叉熵结合

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

p19-p23

欠拟合与过拟合

在这里插入图片描述

在这里插入图片描述

正则化减少过拟合

利用给w加权值,弱化了训练数据的噪声,就是在loss后面加上正则化表达式。

分为L1和L2正则化。

L1正则化:大概率会使很多参数 变为0,因此该方法可以通过稀疏参数,即减少参数的数量,降低复杂度。

L2正则化:会使参数很接近零但不为0,因此该方法可通过减小参数值的大小降低复杂度。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

p29_regularizationcontain.py

# 添加l2正则化
loss_regularization = []
# tf.nn.l2_loss(w)=sum(w ** 2) / 2
loss_regularization.append(tf.nn.l2_loss(w1))
loss_regularization.append(tf.nn.l2_loss(w2))
# 求和
# 例:x=tf.constant(([1,1,1],[1,1,1]))
#   tf.reduce_sum(x)
# >>>6
loss_regularization = tf.reduce_sum(loss_regularization)
loss = loss_mse + 0.03 * loss_regularization  # REGULARIZER = 0.03

优化器更新网络参数

优化器就是引导神经网络更新参数的工具,这节要介绍的就是常用的五种神经网络优化器。

待优化参数W,损失函数loss,学习率lr,每次迭代一个batch,t代表当前batch迭代的总次数。

batch表示一次喂入多少组数据到神经网络,每个batch通常包含2^n组数据。

更新参数一般由以下四步来完成。
不同的优化器只是定义了不同的一阶动量和二阶动量公式而已。

1.计算t时刻 损失函数关于当前参数的梯度gt

2.计算t时刻一阶动量mt和二阶动量Vt

3.计算t时刻下降梯度:nt=lr*mt/根号vt

4.计算t+1时刻参数:Wt+1=wt-nt=wt-lr*mt/根号vt

一阶动量:与梯度相关的函数

二阶动量:与梯度平方相关的函数。

在这里插入图片描述

常用的有:
1.随机梯度下降优化器(SGD)
参数更新公式表示如下图。
代码相比第一部分p45就改动了几行:
直接调用自减函数:assign_sub,实现自更新
跑代码,看代码效果。
代码第二部分p32

在这里插入图片描述

在这里插入图片描述

2.SGDM(含momentum的SGD),在SGD基础上增加一阶动量。
在代码实现时,最重要的就是把一阶动量和二阶动量算出来,根结公式写代码:
参数更新公式:
代码第二部分p34:
加入beta,m_w,m_b的超参数,
然后修改优化器,其余的和SGD都一样

在这里插入图片描述

在这里插入图片描述

3.Adagrad,自适应梯度下降。在SGD基础上增加二阶动量
也就是一阶动量跟SGD一样,二阶动量修改了
然后通过公式推导出参数更新的公式,然后调用变量计算。

在这里插入图片描述

在这里插入图片描述

4.RMSProp,在SGD的基础上增加二阶动量,
只是二阶动量公式不一样
代码p38

在这里插入图片描述

在这里插入图片描述

5.Adam,同时引入了SGDM的一阶动量和RMSProp的二阶动量
并且加入了mt和vt两个修正项,在代码里,一阶动量公式和SGDM是一样的,二阶动量公式和RMSProp是一样的,然后加入两个修正项的计算代码。
代码p40,也就改了优化器,其他的都和SGD一样

在这里插入图片描述

哪个好就用哪个!

第三部分

搭建神经网络并应用

第三讲目标:使用八股搭建神经网络

在这里插入图片描述

在这讲将使用Tensorflow的API接口keras搭建神经网络了。

在这里插入图片描述

keras搭建神经网络只需要六步:
1.导入模块

2.读取训练集,测试集。指定训练集的输入特征和标签,测试集的输入特征和标签。

3.model=tf.keras.modela.Sequential(网络结构)方法,搭建神经网络,逐层描述每层网络,相当于走了一遍前向传播。
Sequential是一个容器,这个容器里封装了一个神经网络结构,函数用法与参数如下图:

在这里插入图片描述

4.model.compile函数,配置神经网络的训练方法,告知训练时选择哪种优化器,哪个损失函数,哪种评测指标,用法与参数如下图:

在这里插入图片描述

有个参数from_logots=flase表示不使用原始输出,也就是经过概率分布后的输出,看激活函数是不是softmax函数吧,如果是,那么神经网络末端输出是经过概率分布的,所以这里必须填false

5.model.fit函数,指定训练过程,告知训练集和测试集的输入特征和标签,每个batch是多少,要迭代多少次数据集
函数用法与参数看下图

在这里插入图片描述

6.model.summary函数,用该函数打印出网络的结构和参数统计

在这里插入图片描述

代码:class3:p8

#第一步:导入模块
import tensorflow as tf
from sklearn import datasets
import numpy as np
#第二步:加载数据集
x_train = datasets.load_iris().data
y_train = datasets.load_iris().target

np.random.seed(116)
np.random.shuffle(x_train)
np.random.seed(116)
np.random.shuffle(y_train)
tf.random.set_seed(116)
#第三步:搭建神经网络(相当于前向传播)
model = tf.keras.models.Sequential([
    tf.keras.layers.Dense(3, activation='softmax', kernel_regularizer=tf.keras.regularizers.l2())#使用全连接层
])
#第四步:配置神经网络的训练方法
model.compile(optimizer=tf.keras.optimizers.SGD(lr=0.1),
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),#不指定输出原始数据,输出的是概率分布
              metrics=['sparse_categorical_accuracy'])
#第五步:指定训练过程
model.fit(x_train, y_train, batch_size=32, epochs=500, validation_split=0.2, validation_freq=20)
#第六步:打印网络的结构和参数统计
model.summary()

用类class搭建一个神经网络结构

对于一些有跳连的非顺序网络结构,我们不能使用刚才的顺序网络结构,这时可以使用类来封装一个神经网络,模块化调用。
这也就是定义了一个类,用类的方式调用函数。
如下图所示:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

MNIST数据集

是一个手写数字的数据集,包含有6w训练集,1w测试集

在这里插入图片描述

使用手写数字识别,仅仅用了十八行代码。

在这里插入图片描述

p13-p14
用类实现mnist数据集辨别手写数字,代码是p15

在这里插入图片描述

FASHION数据集

一共有七万张图,每张都是28*28像素点的灰度值数据。

在这里插入图片描述

其中训练集6W张,测试集1W张,一共十个分类。

在这里插入图片描述

可直接通过keras直接读取数据集。

自己动手实现FASHION数据集的神经网络模型训练。

代码:p16两个文件。

第四部分

在前面已经学会使用六步法搭建神经网络,并使用MNIST数据集与FASHION数据集训练了网络参数,提升识别准确率,但只是训练参数 。在这部分,将增加有用的方法,并对训练好的模型进行应用。

在前面,我们用的都是别人已经打包好的数据集,但如果我们需要实际应用时,必须要有自己的数据集才行,所以在第一小节将介绍如何自制数据集。如果数据集较小,那么模型会出现见识不足,泛化力就较小。所以给出了数据增强的代码,提供泛化力。

如果模型每次运行都从0开始的话,那么所花费的时间将是巨大的,而且在训练中途若出现意外,那么就又得重头再来,所以提出了断点续训,实时保存最优模型。

当我们训练好了 一个模型,他是可以应用在很多方面的,所以提出了参数提取,用文件保存起来,这样在其他应用中也可使用。

再就是acc/loss可视化,用曲线图的形式呈现,可对模型训练的效果以图表的形式进行分析。

最后给出给图识物的应用程序,输入一组新的神经网络从未见过的特征,会输出预测的结果,实现学以致用。

在这里插入图片描述

自制数据集(解决本领域应用)

在class4文件夹下,是具体存在的数据集,数据集中的数据都是28*28分辨率的黑底白字的手写数字图。数据集分60000个训练集和10000个测试集。

在这里插入图片描述

在这里插入图片描述

那么要自制数据集的话,肯定不能再用加载数据集的那两句代码了,我们通过编写函数,来实现对自己数据集的读取与解析。

贴出数据集解析函数:

在这里插入图片描述

整体代码与之前的代码只有数据集读取变化了,神经网络搭建与参数更新函数都没有变化。

import tensorflow as tf
from PIL import Image
import numpy as np
import os

train_path = './mnist_image_label/mnist_train_jpg_60000/'
train_txt = './mnist_image_label/mnist_train_jpg_60000.txt'
x_train_savepath = './mnist_image_label/mnist_x_train.npy'
y_train_savepath = './mnist_image_label/mnist_y_train.npy'

test_path = './mnist_image_label/mnist_test_jpg_10000/'
test_txt = './mnist_image_label/mnist_test_jpg_10000.txt'
x_test_savepath = './mnist_image_label/mnist_x_test.npy'
y_test_savepath = './mnist_image_label/mnist_y_test.npy'


def generateds(path, txt):
    f = open(txt, 'r')  # 以只读形式打开txt文件
    contents = f.readlines()  # 读取文件中所有行
    f.close()  # 关闭txt文件
    x, y_ = [], []  # 建立空列表
    for content in contents:  # 逐行取出
        value = content.split()  # 以空格分开,图片路径为value[0] , 标签为value[1] , 存入列表
        img_path = path + value[0]  # 拼出图片路径和文件名
        img = Image.open(img_path)  # 读入图片
        img = np.array(img.convert('L'))  # 图片变为8位宽灰度值的np.array格式
        img = img / 255.  # 数据归一化 (实现预处理)
        x.append(img)  # 归一化后的数据,贴到列表x
        y_.append(value[1])  # 标签贴到列表y_
        print('loading : ' + content)  # 打印状态提示

    x = np.array(x)  # 变为np.array格式
    y_ = np.array(y_)  # 变为np.array格式
    y_ = y_.astype(np.int64)  # 变为64位整型
    return x, y_  # 返回输入特征x,返回标签y_

#判断路径是否存在,如果不存在就通过generateds重新读取数据,如果存在,说明数据集已经读取过,并保存到.npy文件中,此时直接读取.npy文件就无需再读取数据集,这样就省了很多时间 
if os.path.exists(x_train_savepath) and os.path.exists(y_train_savepath) and os.path.exists(
        x_test_savepath) and os.path.exists(y_test_savepath):
    print('-------------Load Datasets-----------------')
    x_train_save = np.load(x_train_savepath)
    y_train = np.load(y_train_savepath)
    x_test_save = np.load(x_test_savepath)
    y_test = np.load(y_test_savepath)
    x_train = np.reshape(x_train_save, (len(x_train_save), 28, 28))
    x_test = np.reshape(x_test_save, (len(x_test_save), 28, 28))
else:
    print('-------------Generate Datasets-----------------')
    x_train, y_train = generateds(train_path, train_txt)
    x_test, y_test = generateds(test_path, test_txt)

    print('-------------Save Datasets-----------------')
    x_train_save = np.reshape(x_train, (len(x_train), -1))
    x_test_save = np.reshape(x_test, (len(x_test), -1))
    np.save(x_train_savepath, x_train_save)
    np.save(y_train_savepath, y_train)
    np.save(x_test_savepath, x_test_save)
    np.save(y_test_savepath, y_test)

model = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dense(10, activation='softmax')
])

model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
              metrics=['sparse_categorical_accuracy'])

model.fit(x_train, y_train, batch_size=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1)
model.summary()

在程序第一次运行,它会加载所有的图片数据,在加载完所有数据集后,会保存到.npy文件内,下次再进入时,就会先判断

数据增强,扩展数据集

数据增强,可以帮助我们扩展数据集。

对图像的增强,就是对图像的简单形变,用来应对因拍照角度不同引起的图片变形。

Tensorflow2给出了图像数据增强的函数

在这里插入图片描述

因为fit函数需要输入一个四维数据,所以需要对训练输入集进行reshape,把6W张28*28的图像数据,转化成6W张28行28列单通道的图像数据,单通道指的是灰度图像。

在这里插入图片描述

model.fit同步更新为.flow形式,把训练集输入特征、标签特征、按照batch_size=32打包送入model.fit执行训练过程。

代码与第三部分的fashion数据集搭建的神经网络示例,相对比,有如下变化:

在这里插入图片描述

代码:

import tensorflow as tf
from tensorflow.keras.preprocessing.image import ImageDataGenerator

mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)  # 给数据增加一个维度,从(60000, 28, 28)reshape为(60000, 28, 28, 1)

image_gen_train = ImageDataGenerator(
    rescale=1. / 1.,  # 如为图像,分母为255时,可归至0~1
    rotation_range=45,  # 随机45度旋转
    width_shift_range=.15,  # 宽度偏移
    height_shift_range=.15,  # 高度偏移
    horizontal_flip=False,  # 水平翻转
    zoom_range=0.5  # 将图像随机缩放阈量50%
)
image_gen_train.fit(x_train)

model = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dense(10, activation='softmax')
])

model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
              metrics=['sparse_categorical_accuracy'])

model.fit(image_gen_train.flow(x_train, y_train, batch_size=32), epochs=5, validation_data=(x_test, y_test),
          validation_freq=1)
model.summary()

断点续训,存储模型

  • 读取模型

    要想读取模型,可以直接使用Tensorflow给出的load_weights(路径文件名)函数即可读取已有模型。

    使用前先定义模型文件的路径,命名为ckpt文件,因为生成ckpt文件的时候会同步生成索引表,所以通过判断是否存在索引表,即可知道是不是保存过模型参数了。有了索引表就可以调用load_weights函数读取模型参数了。

  • 保存模型

    可以使用Tensorflow给出的回调函数,直接保存训练出来的模型参数,这个函数就是tf.keras.callbacks.ModelCheckpoint参数如下图所示:

在这里插入图片描述

第二个参数表示是否只保留模型参数,第三个参数表示是否只保留最优结果。执行训练过程时,加入callbacks选项,记录到history中

cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path,
                                                 save_weights_only=True,#只保留模型参数
                                                 save_best_only=True)#只保留最优模型

history = model.fit(x_train, y_train, batch_size=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1,
                    callbacks=[cp_callback])#在fit函数中加入回调选项,返回给history

完整代码:

在这里插入图片描述

当我们运行后,会出现:

在这里插入图片描述

出现文件夹说明模型已经保存好了,当我们再次运行时,模型会基于上一次的参数继续进行训练。

参数提取,把参数存入文本

参数提取主要就是把训练好的参数用文件保存起来,这样就可以应用在多个模型内,节省训练时间。

在Tensorflow中,可以使用model.trainable_variables函数返回模型中所有可训练的参数。

要想看到这些参数,可以用print函数直接打印出来,但是直接print的话,中间会出现很多省略号把数据都替换了。

我们可以通过np.set_printoptions(threshold=超过多少省略显示)函数,把参数threshold设置成无限大,就可以把所有参数都显示出来了。具体做法:

在这里插入图片描述

acc/loss可视化,查看训练效果

这讲将介绍如何把准确率上升,把损失函数下降的过程可视化出来。

其实在model.fit执行训练过程时,同步记录了训练集值的损失、测试集的损失、训练集准确率、测试集准确率。

在这里插入图片描述

可以用history.history提取出来,提取代码:

# 通过history.history可提取历史数据
acc = history.history['sparse_categorical_accuracy']#提取训练集准确率
val_acc = history.history['val_sparse_categorical_accuracy']#提取测试集准确率
loss = history.history['loss']#提取训练集loss值
val_loss = history.history['val_loss']#提取测试集loss值

显示训练集与测试集的准确率与loss的曲线。

# 显示训练集和验证集的acc和loss曲线
# 通过history.history可提取历史数据
acc = history.history['sparse_categorical_accuracy']#提取训练集准确率
val_acc = history.history['val_sparse_categorical_accuracy']#提取测试集准确率
loss = history.history['loss']#提取训练集loss值
val_loss = history.history['val_loss']#提取测试集loss值

plt.subplot(1, 2, 1)#将图像分为一行两列
#画出第一列训练集准确率与测试集准确率数据
plt.plot(acc, label='Training Accuracy')
plt.plot(val_acc, label='Validation Accuracy')
#设置标题
plt.title('Training and Validation Accuracy')
#画出第一列
plt.legend()

#画出第二列
plt.subplot(1, 2, 2)
#画出第二列训练集loss与测试集loss数据
plt.plot(loss, label='Training Loss')
plt.plot(val_loss, label='Validation Loss')
#设置标题
plt.title('Training and Validation Loss')
#画出第二列
plt.legend()
#执行绘画
plt.show()

查看效果:

在这里插入图片描述

应用程序,给图识物

到这里就已经掌握了使用Tensorflow的keras神经网络NN的训练方法,但是要实现模型可用,还需要写一套应用程序,实现给图识物。

在这里插入图片描述

Tensorflow提供了一个

model.predict(输入特征,batch_size=整数)函数,它可以根据输入特征,输出预测的计算结果。

有了predict函数,实现给图识物应用,只需三步。

在这里插入图片描述

1.复现模型,也就是使用Sequential搭建神经网络

2.加载参数,model.load_weights(model_save_path),如果我们之前没有保存过参数,那么还需要重新设定模型参数训练model.compile,再执行训练过程model.fit。

3.预测结果,result = model.predict(x_predict),也就是根据输入特征预测输出结果。

通过这三步就可以了。

接下来设计了一个应用程序,把文件夹中的十张图片给模型,让他识别。

代码:

from PIL import Image
import numpy as np
import tensorflow as tf

model_save_path = './checkpoint/mnist.ckpt'
#搭建神经网络,复现神经网络
model = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(128, activation='relu'),
    tf.keras.layers.Dense(10, activation='softmax')])

#加载已经训练好的参数
model.load_weights(model_save_path)
#询问要测试多少张图片
preNum = int(input("请输入需要测试的图片的个数:"))
#循环
for i in range(preNum):
    image_path = input("请输入图片路径:")
    img = Image.open(image_path)
    # 因为训练是使用28行28列的灰度图,输入是任意尺寸图片,所以需要进行转换成28*28的标准尺寸,再转换成灰度图。
    img = img.resize((28, 28), Image.ANTIALIAS)
    img_arr = np.array(img.convert('L'))
    # 输入的图片是白底黑字的,但我们训练时用的是黑底白字的图,所以需要进行像素转化。
    img_arr = 255 - img_arr
    '''
    # 和p27唯一不同就是对图像像素处理,这段循环程序是将输入图像变换成只有黑色与白色的高对比图片
    这种方法在保留图像有用信息的同时,滤去了背景噪声,图片更干净,识别效果会更好!
    for i in range(28):
        for j in range(28):
            if img_arr[i][j] < 200:
                img_arr[i][j] = 255
            else:
                img_arr[i][j] = 0
    '''
    #再对图像进行除以255,归一化操作
    img_arr = img_arr / 255.0
    print("img_arr:",img_arr.shape)
    #由于神经网络训练时,都是按照batch送入网络的,所以进入predict函数前,先要在img_arr前添加一个维度,1*28*28的三维数据。
    x_predict = img_arr[tf.newaxis, ...]
    print("x_predict:",x_predict.shape)
    #再送入predict预测
    result = model.predict(x_predict)
    #把最大的概率值输出
    pred = tf.argmax(result, axis=1)
    #返回预测结果
    print('\n')
    tf.print(pred)

到这里,基本可以做应用了,想想有什么带数据有带标签的,都可以进行训练。

第五部分

在前面我们学会使用六步法搭建了全连接网络,训练了MNIST数据集和FASHION数据集,实现了图像识别应用。

在这里插入图片描述

卷积计算过程

在全连接网络组中,我们输入的是28*28分辨率的图像,那么输入就有784个像素点,在全连接网络中,每个神经元与前后相邻的每一个神经元都是有链接关系的,照这么说的话,参数如果是128个的话,那么我们第一层参数就有784X128+128个b的参数,第二层的话(也就是输出与参数的关系)就有128X10个w+10个b,一共101770个参数。

在这里插入图片描述

如果输入换成了高分辨率的彩色图像,不仅像素点会增加,而且还从灰度图的单通道信息,变成了彩色图红绿蓝三通道信息,那么待优化的参数过多很容易导致模型过拟合。

为了避免过拟合的发生,我们往往不会把原始图像喂入到全连接神经网络,通常做法是会先对原始图像进行特征提取,再把提取到的特征喂给全连接网络。再让全连接网络按照之前的方法计算出分类评估值。

卷积

卷积就是一种有效提取图像特征的方法。一般会用一个正方形的卷积核,在输入图上滑动,遍历图片上的每个点。图片区域内相对应的每一个像素值,乘以卷积核内相对应点的权重,求和,再加上偏置。
例如现在有一个5X5X1的灰度图像(1表示灰度图像,单通道。5X5表示图像的分辨率),我们可以用一个3X3X1的正方形卷积核在输入图像上滑动,遍历图像的每个点,那么输出上的点,就是图片区域内,相对应的每一个像素值,乘以卷积核内相应点的权重,求和,再加上偏置。(具体看图)
输出图片的边长=(输入图片边长-卷积核长+1)/步长。

输入特征图的深度(通道数)决定了当前层卷积核的深度。

当前层卷积核的个数决定了当前层输出特征图的深度。如果觉得某曾模型的特征提取能力不足,可以在这层多用几个卷积核提高这一层的特征提取能力。

在这里插入图片描述

再看看卷积核:

在这里插入图片描述

这三个卷积核,里面的每个小颗粒都存储着一个待训练参数,在执行卷积计算时,卷积核里的这些参数是固定的,在每次反向传播时,这些小颗粒中存储的待训练参数会被梯度下降法更新,卷积就是利用立体卷积核,实现了参数的空间共享。

卷积的计算过程:

输入特征图是单通道:

在这里插入图片描述

输入特征图是三通道的:

在这里插入图片描述

三通道时,如果步长是1,那么每滑动一步,输入特征图与卷积核里的27个元素重合,他们对应元素相乘再加上偏置项b。比如卷积核滑动到输入特征的中心 ,那么相应的三个通道的输入图像,分别与卷积的三层特征数据重合,他们对应元素相乘再加上偏置项b就得到输出图的一个像素值 ,完成卷积计算的过程。

在这里插入图片描述

当有n个卷积核时,会有n张输出特征图,叠加在这张输出特征图的后面。

感受野

感受野是指输出特征图中1个像素点,在原始输入图片上的映射区域的大小。

一层3X3的卷积核,在输入5X5的原始图像上,感受野是3,因为他左右移动只能3次,只能一次感受三个原始图像像素点。两层3X3卷积核在原始图像上的感受野是5,一层5X5的卷积核在原始图像上的感受野也是5。

在这里插入图片描述

全零填充

有时我们会对输入图像进行全零填充,这样会使得输出图像与输入图像分辨率一致
在Tensorflow中,pading=SAME表示使用全零填充,pading=VALID表示不使用全零填充。

在这里插入图片描述

计算公式:

在这里插入图片描述

TF描述卷积层

Tensorflow给出了计算卷积的函数。

tf.keras.layers.Conv2D函数,其参数如下图:

在这里插入图片描述

注意:在activation激活函数参数项那儿,如果这层卷积后还有批标准化操作,那么不在这个参数写激活函数。

批标准化

神经网络对0附近的数据更敏感,但是随着网络层数的增加,特征数据会偏离0均值的情况,标准化可以使数据符合以0为均值,1为标准差的标准正态分布,把偏移的特征数据重新拉回到0附近。

批标准化是对一个batch的数据做标准化处理,使数据回归标准正态分布,常用在卷积操作和激活操作之间。

在这里插入图片描述

Hik表示第k个卷积核输出特征图中的第i个像素点。

批标准化会让每个像素点进行减均值除以标准差的 自更新计算。

批标准化会将原本偏移的特征数据,重新拉回到0均值,使进入激活函数的数据分布在激活函数线性区,使得输入数据的微小变化更明显的体现到激活函数的输出,提升了激活函数对输入数据的区分力,但是这种简单的特征数据标准化,使特征数据完全满足标准正态分布,集中在激活函数中心的线性区域,使激活函数丧失了非线性特性,因此在批标准化操作中,为每个卷积核引入了两个可训练参数:缩放因子和偏移因子。

在这里插入图片描述

在反向传播时,缩放因子和偏移因子会与其他待训练参数一同被训练优化,使标准正态分布后的特征数据,通过缩放因子和偏移因子优化了特征数据分布的宽窄和偏移量,保证了网络的非线性表达力。

BN层位于卷积层之后,激活层之前。

在这里插入图片描述

Tensorflow提供了BN操作的函数BatchNormalization(),参考上面代码的写法,把BN加到卷积层与激活层之间。

池化

赤化操作用于减少特征数据量,赤化的方法主要有最大池化均值池化

1.最大池化:最大池化可以提取图片纹理。
过程:假设输入为4X4的图片,使用2X2的核,以步长为2,对输入图片进行池化,那么就会从输入图像中2X2位置选出最大的那个数作为输出,从而输出图片便为输入图片的四分之一。
2.均值池化:均值池化可以保留背景特征。
均值池化就是从2*2位置内取出四个数据的均值作为输出

在这里插入图片描述

Tensorflow给出了池化的函数:

最大池化:tf.keras.layers.MAXPool2D

均值池化:tf.keras.layers.AveragePooling2D

具体参数见下图:

在这里插入图片描述

舍弃(Dropout)

在神经网络的训练过程中,为了减少过多的参数,常使用Dropout的方法,将一部分神经元按照一定概率从神经网络中暂时舍弃
这种舍弃是临时性的,只在训练时舍弃一定概率的神经元,在使用神经网络时会将被舍弃的神经元恢复到神经网络中。
Dropout可以有效减少过拟合。常常在前向传播构建神经网络时,使用Dropout,加快模型的训练速度,Dropout一般放在全连接网络中

tensorflow提供了函数:
tf.nn.dropout(上层输出,暂时舍弃的概率)

在这里插入图片描述

0.2表示随机舍弃20%的神经元。

卷积神经网络

到这,卷积神经网络就介绍完了。

卷积神经网络就是借助卷积核对输入特征进行特征提取,再把提取到的特征喂入全连接网络进行识别预测

提取特征包括卷积、批标准化、激活、池化四步

提问:卷积是什么?

答:卷积就是特征提取器,就是CBAPD

在这里插入图片描述

Cifar10数据集(6W张彩色图片)

Cifar10数据集内有6W张彩色图片,每张图片有32行32列像素点的红绿蓝三通道数据。

5W张用于训练,1W张用于测试,十个分类分别是飞机、汽车、鸟、猫、鹿、狗、青蛙、马和卡车。
在这里插入图片描述

通过直接导入数据集获取训练集与测试集。

可以使用plt把训练集中的第一个样本可视化出来。然后打印一下这个样本的数据,这个数据就是这张青蛙图片的32行32列个像素点的RGB值。

然后打印标签,6对应是青蛙,还可以打印出形状,是10000个32行32列的RGB三通道数据,四个维度。

在这里插入图片描述

使用卷积神经网络训练cifar10数据集

搭建一个一层卷积,两层全连接的网络,使用6个5X5的卷积核,过2X2的池化核,池化步长是2,然后过128个神经元的全连接层,由于Cifar10是10分类,所以最后还要过一个一层十个神经元的全连接层。

搭建卷积神经网络的八股口诀是CBAPD,把CBAPD填上具体的值就是如下图所示:

最后一个标识过一层十个神经元的全连接层,通过softmax函数使输出符合概率分布。

在这里插入图片描述

由于网络相对复杂了,所以使用class类搭建神经网络。

在这里插入图片描述

具体代码如下图:

在这里插入图片描述

在下面介绍经典卷积神经网络结构时,只修改如上图方框内的东西,其他都不变。

然后在代码跑完之后,会存储所有参数到weights.txt文件中,有了这些参数,就可以在任何平台复现出神经网络的前向传播实现应用。

经典卷积网络

在这里插入图片描述

Lenet

LeNet通过共享卷积核,减少了网络的参数。

在统计卷积神经网络层数时,一般只统计卷积计算层和全连接计算层,其余操作可认为是卷积计算层的附属。

LeNet一共有五层网络

在这里插入图片描述

在这里插入图片描述

整体代码除了这个类,其他都与上节代码一模一样:

#这个类是对LeNet5网络的描述
class LeNet5(Model):
    def __init__(self):
        super(LeNet5, self).__init__()
        self.c1 = Conv2D(filters=6, kernel_size=(5, 5),
                         activation='sigmoid')
        self.p1 = MaxPool2D(pool_size=(2, 2), strides=2)

        self.c2 = Conv2D(filters=16, kernel_size=(5, 5),
                         activation='sigmoid')
        self.p2 = MaxPool2D(pool_size=(2, 2), strides=2)

        self.flatten = Flatten()
        self.f1 = Dense(120, activation='sigmoid')
        self.f2 = Dense(84, activation='sigmoid')
        self.f3 = Dense(10, activation='softmax')

    def call(self, x):
        x = self.c1(x)
        x = self.p1(x)

        x = self.c2(x)
        x = self.p2(x)

        x = self.flatten(x)
        x = self.f1(x)
        x = self.f2(x)
        y = self.f3(x)
        return y

运行代码得到结果

loss: 1.5139 - sparse_categorical_accuracy: 0.4468 - val_loss: 1.4782 - val_sparse_categorical_accuracy: 0.4583

训练集准确率是44%,测试集准确率是45%,可见准确率并不高。

在这里插入图片描述

AlexNet

Top5错误率为16.4%,使用relu激活函数,提升了训练速度,使用Dropout缓解了过拟合。

AlexNet共有八层,第一层使用了96个3X3的卷积核,步长为1,不使用全零填充,特征标准化使用了LRN,但近年来LRN使用的很少,所以我们自己使用BN批标准化实现特征标准化,使用relu激活函数,用3X3的池化核,步长是2做最大池化,不使用Dropout。

其它层对照图来看。

在这里插入图片描述

在代码中,还是只修改类结构,如下,用框图表示,类中每个框图代表一层,对着左边神经网络八股,写出代码如图右边。

在这里插入图片描述

#对网络结构描述
class AlexNet8(Model):
    def __init__(self):
        super(AlexNet8, self).__init__()
        self.c1 = Conv2D(filters=96, kernel_size=(3, 3))
        self.b1 = BatchNormalization()
        self.a1 = Activation('relu')
        self.p1 = MaxPool2D(pool_size=(3, 3), strides=2)

        self.c2 = Conv2D(filters=256, kernel_size=(3, 3))
        self.b2 = BatchNormalization()
        self.a2 = Activation('relu')
        self.p2 = MaxPool2D(pool_size=(3, 3), strides=2)

        self.c3 = Conv2D(filters=384, kernel_size=(3, 3), padding='same',
                         activation='relu')
                         
        self.c4 = Conv2D(filters=384, kernel_size=(3, 3), padding='same',
                         activation='relu')
                         
        self.c5 = Conv2D(filters=256, kernel_size=(3, 3), padding='same',
                         activation='relu')
        self.p3 = MaxPool2D(pool_size=(3, 3), strides=2)

        self.flatten = Flatten()
        self.f1 = Dense(2048, activation='relu')
        self.d1 = Dropout(0.5)
        self.f2 = Dense(2048, activation='relu')
        self.d2 = Dropout(0.5)
        self.f3 = Dense(10, activation='softmax')

    def call(self, x):
        x = self.c1(x)
        x = self.b1(x)
        x = self.a1(x)
        x = self.p1(x)

        x = self.c2(x)
        x = self.b2(x)
        x = self.a2(x)
        x = self.p2(x)

        x = self.c3(x)

        x = self.c4(x)

        x = self.c5(x)
        x = self.p3(x)

        x = self.flatten(x)
        x = self.f1(x)
        x = self.d1(x)
        x = self.f2(x)
        x = self.d2(x)
        y = self.f3(x)
        return y

然后运行代码可知,识别准确率比LeNet高。

VGGNet

Top5错误率减小到了7.3%,使用小卷积核,在减少参数的同时,提高了识别准确率,VGGNet的网络结构规整,非常适合硬件加速,以16层VGG网络为例。

在这里插入图片描述

代码:

class VGG16(Model):
    def __init__(self):
        super(VGG16, self).__init__()
        self.c1 = Conv2D(filters=64, kernel_size=(3, 3), padding='same')  # 卷积层1
        self.b1 = BatchNormalization()  # BN层1
        self.a1 = Activation('relu')  # 激活层1
        self.c2 = Conv2D(filters=64, kernel_size=(3, 3), padding='same', )
        self.b2 = BatchNormalization()  # BN层1
        self.a2 = Activation('relu')  # 激活层1
        self.p1 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
        self.d1 = Dropout(0.2)  # dropout层

        self.c3 = Conv2D(filters=128, kernel_size=(3, 3), padding='same')
        self.b3 = BatchNormalization()  # BN层1
        self.a3 = Activation('relu')  # 激活层1
        self.c4 = Conv2D(filters=128, kernel_size=(3, 3), padding='same')
        self.b4 = BatchNormalization()  # BN层1
        self.a4 = Activation('relu')  # 激活层1
        self.p2 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
        self.d2 = Dropout(0.2)  # dropout层

        self.c5 = Conv2D(filters=256, kernel_size=(3, 3), padding='same')
        self.b5 = BatchNormalization()  # BN层1
        self.a5 = Activation('relu')  # 激活层1
        self.c6 = Conv2D(filters=256, kernel_size=(3, 3), padding='same')
        self.b6 = BatchNormalization()  # BN层1
        self.a6 = Activation('relu')  # 激活层1
        self.c7 = Conv2D(filters=256, kernel_size=(3, 3), padding='same')
        self.b7 = BatchNormalization()
        self.a7 = Activation('relu')
        self.p3 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
        self.d3 = Dropout(0.2)

        self.c8 = Conv2D(filters=512, kernel_size=(3, 3), padding='same')
        self.b8 = BatchNormalization()  # BN层1
        self.a8 = Activation('relu')  # 激活层1
        self.c9 = Conv2D(filters=512, kernel_size=(3, 3), padding='same')
        self.b9 = BatchNormalization()  # BN层1
        self.a9 = Activation('relu')  # 激活层1
        self.c10 = Conv2D(filters=512, kernel_size=(3, 3), padding='same')
        self.b10 = BatchNormalization()
        self.a10 = Activation('relu')
        self.p4 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
        self.d4 = Dropout(0.2)

        self.c11 = Conv2D(filters=512, kernel_size=(3, 3), padding='same')
        self.b11 = BatchNormalization()  # BN层1
        self.a11 = Activation('relu')  # 激活层1
        self.c12 = Conv2D(filters=512, kernel_size=(3, 3), padding='same')
        self.b12 = BatchNormalization()  # BN层1
        self.a12 = Activation('relu')  # 激活层1
        self.c13 = Conv2D(filters=512, kernel_size=(3, 3), padding='same')
        self.b13 = BatchNormalization()
        self.a13 = Activation('relu')
        self.p5 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same')
        self.d5 = Dropout(0.2)

        self.flatten = Flatten()
        self.f1 = Dense(512, activation='relu')
        self.d6 = Dropout(0.2)
        self.f2 = Dense(512, activation='relu')
        self.d7 = Dropout(0.2)
        self.f3 = Dense(10, activation='softmax')

    def call(self, x):
        x = self.c1(x)
        x = self.b1(x)
        x = self.a1(x)
        x = self.c2(x)
        x = self.b2(x)
        x = self.a2(x)
        x = self.p1(x)
        x = self.d1(x)

        x = self.c3(x)
        x = self.b3(x)
        x = self.a3(x)
        x = self.c4(x)
        x = self.b4(x)
        x = self.a4(x)
        x = self.p2(x)
        x = self.d2(x)

        x = self.c5(x)
        x = self.b5(x)
        x = self.a5(x)
        x = self.c6(x)
        x = self.b6(x)
        x = self.a6(x)
        x = self.c7(x)
        x = self.b7(x)
        x = self.a7(x)
        x = self.p3(x)
        x = self.d3(x)

        x = self.c8(x)
        x = self.b8(x)
        x = self.a8(x)
        x = self.c9(x)
        x = self.b9(x)
        x = self.a9(x)
        x = self.c10(x)
        x = self.b10(x)
        x = self.a10(x)
        x = self.p4(x)
        x = self.d4(x)

        x = self.c11(x)
        x = self.b11(x)
        x = self.a11(x)
        x = self.c12(x)
        x = self.b12(x)
        x = self.a12(x)
        x = self.c13(x)
        x = self.b13(x)
        x = self.a13(x)
        x = self.p5(x)
        x = self.d5(x)

        x = self.flatten(x)
        x = self.f1(x)
        x = self.d6(x)
        x = self.f2(x)
        x = self.d7(x)
        y = self.f3(x)
        return y

运行代码发现,准确率达到了百分之七十多

InceptionNet

Top5错误率为6.67%,引入了Inception结构块,在同一层网络内使用不同尺寸的卷积核,提升了模型的感知力,使用批标准化缓解了梯度消失。

它的核心是他的基本单元Inception结构块,无论是GoogleNet(Inception V1)还是Inception的后续版本(V2,V3,V4)都是基于Inception结构块搭建的卷积神经网络,Inception结构块在同一层网络中使用了多个尺寸的卷积核,可以提取不同尺寸的特征。

通过1X1的卷积核作用到输入特征图的每个像素点,通过设定少于特征图深度的1X1卷积核个数,减少了输出特征图深度,起到了降为的作用,减少了参数量与计算量,这一页给出了Inception结构块,Inception结构块包含四个分支,分别经过1X1卷积核输出到卷积连接器,经过1X1卷积和配合3X3卷积核输出到卷积连接器,经过1X1卷积和配合5X5卷积核输出到卷积连接器,经过3X3最大池化层配13X1卷积核输出到卷积连接器,送到卷积连接器的特征数据尺寸相同,卷积连接器会把收到的这四路特征数据按深度方向进行拼接,形成Inception结构块的输出。

在这里插入图片描述

从左到右数是第一个分支到第四个分支,然后把四个分支堆叠,再输出,代码解释图:

在这里插入图片描述

代码:

class InceptionBlk(Model):
    def __init__(self, ch, strides=1):
        super(InceptionBlk, self).__init__()
        self.ch = ch
        self.strides = strides
        self.c1 = ConvBNRelu(ch, kernelsz=1, strides=strides)#第一个分支
        self.c2_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)#第二个分支,使用两次ConvBNRelu卷积操作
        self.c2_2 = ConvBNRelu(ch, kernelsz=3, strides=1)#第二个分支,使用两次ConvBNRelu卷积操作
        self.c3_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)#第三个分支,使用两次ConvBNRelu卷积操作
        self.c3_2 = ConvBNRelu(ch, kernelsz=5, strides=1)
        self.p4_1 = MaxPool2D(3, strides=1, padding='same')#第四个分支,使用两次ConvBNRelu卷积操作
        self.c4_2 = ConvBNRelu(ch, kernelsz=1, strides=strides)

    def call(self, x):
        x1 = self.c1(x)
        x2_1 = self.c2_1(x)
        x2_2 = self.c2_2(x2_1)
        x3_1 = self.c3_1(x)
        x3_2 = self.c3_2(x3_1)
        x4_1 = self.p4_1(x)
        x4_2 = self.c4_2(x4_1)
        # concat along axis=channel
        x = tf.concat([x1, x2_2, x3_2, x4_2], axis=3)#x1,x2_2, x3_2, x4_2是四个分支的输出,使用tf.concat函数将他们堆叠,axis=3指定堆叠的维度是沿深度方向,最终输出
        return x

有了结构块后,就可以搭建出一个精简版本的InceptionNet了,网络共有十层。

在这里插入图片描述

代码如下所示:

其改动代码有fit函数的batch_size,把batch_size从32调整到了1024,因为网络规模较大,让一次喂入神经网络的数据量多一些

class ConvBNRelu(Model):
    def __init__(self, ch, kernelsz=3, strides=1, padding='same'):
        super(ConvBNRelu, self).__init__()
        self.model = tf.keras.models.Sequential([
            Conv2D(ch, kernelsz, strides=strides, padding=padding),
            BatchNormalization(),
            Activation('relu')
        ])

    def call(self, x):
        x = self.model(x, training=False) #在training=False时,BN通过整个训练集计算均值、方差去做批归一化,training=True时,通过当前batch的均值、方差去做批归一化。推理时 training=False效果好
        return x

#Inception结构块的定义
class InceptionBlk(Model):
    def __init__(self, ch, strides=1):
        super(InceptionBlk, self).__init__()
        self.ch = ch
        self.strides = strides
        self.c1 = ConvBNRelu(ch, kernelsz=1, strides=strides)#第一个分支
        self.c2_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)#第二个分支,使用两次ConvBNRelu卷积操作
        self.c2_2 = ConvBNRelu(ch, kernelsz=3, strides=1)#第二个分支,使用两次ConvBNRelu卷积操作
        self.c3_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)#第三个分支,使用两次ConvBNRelu卷积操作
        self.c3_2 = ConvBNRelu(ch, kernelsz=5, strides=1)
        self.p4_1 = MaxPool2D(3, strides=1, padding='same')#第四个分支,使用两次ConvBNRelu卷积操作
        self.c4_2 = ConvBNRelu(ch, kernelsz=1, strides=strides)

    def call(self, x):
        x1 = self.c1(x)
        x2_1 = self.c2_1(x)
        x2_2 = self.c2_2(x2_1)
        x3_1 = self.c3_1(x)
        x3_2 = self.c3_2(x3_1)
        x4_1 = self.p4_1(x)
        x4_2 = self.c4_2(x4_1)
        # concat along axis=channel
        x = tf.concat([x1, x2_2, x3_2, x4_2], axis=3)#x1,x2_2, x3_2, x4_2是四个分支的输出,使用tf.concat函数将他们堆叠,axis=3指定堆叠的维度是沿深度方向,最终输出
        return x

#Inception10卷积神经网络的搭建
class Inception10(Model):
    def __init__(self, num_blocks, num_classes, init_ch=16, **kwargs):
        super(Inception10, self).__init__(**kwargs)
        self.in_channels = init_ch
        self.out_channels = init_ch
        self.num_blocks = num_blocks
        self.init_ch = init_ch
        self.c1 = ConvBNRelu(init_ch)
        self.blocks = tf.keras.models.Sequential()
        for block_id in range(num_blocks):
            for layer_id in range(2):
                if layer_id == 0:
                    block = InceptionBlk(self.out_channels, strides=2)
                else:
                    block = InceptionBlk(self.out_channels, strides=1)
                self.blocks.add(block)
            # enlarger out_channels per block
            self.out_channels *= 2
        self.p1 = GlobalAveragePooling2D()#平均池化
        self.f1 = Dense(num_classes, activation='softmax')#

    def call(self, x):
        x = self.c1(x)
        x = self.blocks(x)
        x = self.p1(x)
        y = self.f1(x)
        return y


model = Inception10(num_blocks=2, num_classes=10)#num_blocks指定了block数,这里只有block_0和block_1,num_classes指定了网络是几分类的,因为最后输出是10,所以是十分类的

最终训练集准确率是67%,测试集准确率很低是17%

ResNet

Top5错误率为3.57%,提出了层间残差跳连,引入了前方信息,缓解梯度消失,是神经网络层数增加成为可能,纵览之前讲过的神经网络层数:

在这里插入图片描述

可见,通过加深网络层数,取得更好的效果,但是单纯堆叠网络层数,会使神经网络模型退化,以至于后面的特征丢失了前面特征的原本模样,于是他将前面的特征通过跳线直接接到了后面:

在这里插入图片描述

如图示所示,H(x)输出是F(x)与x的叠加(但是在ResNet中是特征图对应元素相加,也就是矩阵值相加),正反馈,有效缓解了神经网络模型堆叠导致的退化,使得神经网络可以向着更深层级发展。

在这里插入图片描述

ResNet块:

在这里插入图片描述

在这个图中解释了代码流程:

如果堆叠卷积层前后维度不同,residual_path=1,调用红色块的代码,使用1X1卷积操作,调整输入特征图inputs的尺寸或深度后,将堆叠卷积输出特征y和if语句计算出的residual相加,过激活,输出。

如果堆叠卷积层前后维度相同,不执行红色块内的代码,直接将堆叠卷积输出特征y和inputs相加,过激活,输出。

然后使用写出的ResNet块,搭建这个网络结构。

在这里插入图片描述

八个ResNet块,最后是一层全连接,每一个ResNet块有两层卷积,一共是十八层网络。

代码看class5:p46

小结

在这里插入图片描述

第六部分

本节将介绍循环神经网络RNN实现连续数据的预测,将以股票预测为例。

在这里插入图片描述

然后有些数据是与时间序列相关的,是可以根据上文预测出下文的。比如鱼离不开___,下意识就预测出了水字,这种预测在人脑中就是通过脑记忆体提取历史数据的特征,预测出最可能的情况。

然后脑记忆体,就是本节需要介绍的循环核。

循环核

循环核:参数时间共享,循环层提取时间信息。

循环核具有记忆力,通过不同时刻的参数共享,实现了对时间序列的信息提取,

在这里插入图片描述

循环核按时间步展开

循环核按照时间步展开,就是把循环核按照时间轴方向展开。表示为如下图:

每个时刻,记忆体状态信息ht被刷新,记忆体周围的参数矩阵Wxh、Whh、Why是固定不变的,我们训练优化的,就是这些参数矩阵。

训练完成后,使用效果最好的参数矩阵,执行前向传播,输出预测结果。

这跟人脑的记忆体是一样的,每个时刻都根据当前的输入而更新。当前的预测推理,是根据你以往的知识积累,用固化下来的参数矩阵进行推理判断。

而循环神经网络:就是通过循环核提取时间特征后,送入全连接网络,实现连续数据的预测

yt是整个循环网络的末层,从公式上来看就是一个全连接网络,借助全连接网络,实现连续数据的预测。

在这里插入图片描述

循环计算层:向着输出方向生长

每个循环核构成一层循环计算层,循环计算层的层数是向着输出方向增长的。

每个循环核中的记忆体的个数是根据你的需求任意来定的。

在这里插入图片描述

TF描述循环计算层

Tensorflow提供了计算循环层的函数,tf.keras.layers.SimpleRNN,具体参数如下图所示。

例如:SimpleRNN(3,return_sequences=True)表示定义了一个具有三个记忆体的循环核,这个循环核会在每个时间步输出ht。

在这里插入图片描述

循环核在每个时间步输出ht,可以如下图表示。

在这里插入图片描述

循环核仅在最后一个时间步输出ht,如图示,循环核在每个时间步后不进行输出,到最后:

在这里插入图片描述

在使用 函数也是有要求的,需要指定送入样本书,循环核时间展开步数,每个时间步输入特征个数,如下图所示。

循环核时间展开步数表示:每组数据经过几个时间步就会得到输出结果。

以左边的为例,输入特征为两组,每组数据经过一个时间步就会得到输出结果,每组数据有三个输入特征,如下图

在这里插入图片描述

循环网络的计算过程

接下来用一个案例,对循环网络的计算过程进行讲解。

字母预测案例:输入a预测出b,输入b预测出c,输入c预测出d,输入d预测出e,输入e预测出a。

首先,因为神经网络输入的都是数字,所以,我们先把abcde五个字母通过独热码编码,成如下结果:

在这里插入图片描述

现在我们输入b对应的词向量空间 ,01000,作为xt输入,那么循环神经网络会随机生成Wxh、Whh、Why三个参数矩阵,记忆体的个数选取3,此时循环核中记忆体状态信息初始化为0,0,0。

然后开机计算记忆体状态信息ht=tanh(xtWxh+ht-1Whh+bh),再通过tanh激活函数,得到当前时刻的状态信息ht(这个过程可以理解为人脑中的记忆,因为当前输入的事物而更新了),此时记忆体状态信息就被刷新为刚刚计算的结果。

在这里插入图片描述

输出yt就是把提取到的时间信息ht,通过全连接进行识别预测的过程。

计算yt=softmax(htWhy+by),在通过softmax激活函数得出计算结果,由结果可知:

在这里插入图片描述

可见模型认为有91%的可能输出字母c。

在这里插入图片描述

字母预测onehot_1pre1

使用RNN实现输入一个字母,预测下一个字母,使用Onehot编码。

代码分几个部分讲解:

第一个部分加载训练集:

#输入数据
input_word = "abcde"
w_to_id = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4}  # 单词映射到数值id的词典
#独热码编码
id_to_onehot = {0: [1., 0., 0., 0., 0.], 1: [0., 1., 0., 0., 0.], 2: [0., 0., 1., 0., 0.], 3: [0., 0., 0., 1., 0.],
                4: [0., 0., 0., 0., 1.]}  # id编码为one-hot
#生成训练用的特征x_train和标签y_train,a对应b,b对应c,....,e对应a
x_train = [id_to_onehot[w_to_id['a']], id_to_onehot[w_to_id['b']], id_to_onehot[w_to_id['c']],
           id_to_onehot[w_to_id['d']], id_to_onehot[w_to_id['e']]]
y_train = [w_to_id['b'], w_to_id['c'], w_to_id['d'], w_to_id['e'], w_to_id['a']]

第二个部分:

# 使x_train符合SimpleRNN输入要求:[送入样本数, 循环核时间展开步数, 每个时间步输入特征个数]。
# 此处整个数据集送入,送入样本数为len(x_train);输入1个字母出结果,循环核时间展开步数为1; 表示为独热码有5个输入特征,每个时间步输入特征个数为5
x_train = np.reshape(x_train, (len(x_train), 1, 5))
y_train = np.array(y_train)#变为numpy格式

第三个部分:

#搭建具有三个记忆体的循环层,三个是我随意选的,记忆体个数越多,记忆力更好,但消耗更多资源
model = tf.keras.Sequential([
    SimpleRNN(3),
    Dense(5, activation='softmax')#全连接,实现输出层yt的计算
])

接下来就与之前的卷积神经网络的反向传播过程差不多了

#配置RNN训练参数
model.compile(optimizer=tf.keras.optimizers.Adam(0.01),
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
              metrics=['sparse_categorical_accuracy'])
#断点续训,读取保存好的ckpt.index指标文件
checkpoint_save_path = "./checkpoint/rnn_onehot_1pre1.ckpt"

if os.path.exists(checkpoint_save_path + '.index'):
    print('-------------load the model-----------------')
    model.load_weights(checkpoint_save_path)
#在调用fit执行训练过程后,使用这个回调函数实现断点续训
cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path,
                                                 save_weights_only=True,
                                                 save_best_only=True,
                                                 monitor='loss')  # 由于fit没有给出测试集,不计算测试集准确率,根据loss,保存最优模型
#执行训练过程
history = model.fit(x_train, y_train, batch_size=32, epochs=300, callbacks=[cp_callback])
#打印网络结构与参数
model.summary()

# print(model.trainable_variables)
#实现参数提取
file = open('./weights.txt', 'w')  # 参数提取
for v in model.trainable_variables:
    file.write(str(v.name) + '\n')
    file.write(str(v.shape) + '\n')
    file.write(str(v.numpy()) + '\n')
file.close()

###############################################    show   ###############################################

# 显示训练集和验证集的acc和loss曲线
acc = history.history['sparse_categorical_accuracy']
loss = history.history['loss']

plt.subplot(1, 2, 1)
plt.plot(acc, label='Training Accuracy')
plt.title('Training Accuracy')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(loss, label='Training Loss')
plt.title('Training Loss')
plt.legend()
plt.show()

############### predict #############
#测试
preNum = int(input("input the number of test alphabet:"))
for i in range(preNum):
    alphabet1 = input("input test alphabet:")#输入字母
    alphabet = [id_to_onehot[w_to_id[alphabet1]]]#把字母编码成独热码
    # 使alphabet符合SimpleRNN输入要求:[送入样本数, 循环核时间展开步数, 每个时间步输入特征个数]。
    # 此处验证效果送入了1个样本,送入样本数为1;输入1个字母出结果,所以循环核时间展开步数为1;
    # 表示为独热码有5个输入特征,每个时间步输入特征个数为5
    alphabet = np.reshape(alphabet, (1, 1, 5))#编码成RNN希望的形状
    result = model.predict([alphabet])
    pred = tf.argmax(result, axis=1)
    pred = int(pred)
    tf.print(alphabet1 + '->' + input_word[pred])

循环计算过程II(连续输入多个字母预测下一个字母)

下面我们把时间核按时间步展开,连续输入多个字母预测下一个字母的例子。

以连续输入四个字母,预测下一个字母为例,讲解循环核按时间展开后的循环计算过程。

我们通过已经训练好的网络参数,感受循环计算的前向传播过程,在这个过程中的每个时刻参数矩阵是固定的,每个时刻的记忆体被更新。

在第一个时刻,使用b的独热码作为输入,记忆体根据更新公式刷新了。第二个时刻,c的独热码输入,根据更新公式再次对记忆体更新。在第三个时刻,d的独热码输入,又再次对记忆体进行更新,第四时刻e的独热码输入,记忆体又更新了,最后在第四时刻进行输出运算,输入预测通过全连接完成,代入ye计算公式,得出结果。

在这里插入图片描述

Embedding(一种编码方法)

在上面的例子中,使用独热码对5个字母进行编码,独热码的位宽要与词汇量一致,如果词汇量增大时,非常浪费资源,所以可以使用另一种编码。

Embedding是一种专用在单词的编码方法。

实现编码的函数与参数如图所示:

在这里插入图片描述

Embedding层对输入数据的维度也有要求,要求输入数据是二维的。第一维告知送入几个样本,第二维是循环核时间展开步数。

单字母预测

如下代码进行修改,其余的都和使用独热码编码方式预测下一个单个字母代码一致。

# 使x_train符合Embedding输入要求:[送入样本数, 循环核时间展开步数] ,
# 此处整个数据集送入所以送入,送入样本数为len(x_train);输入1个字母出结果,循环核时间展开步数为1。
x_train = np.reshape(x_train, (len(x_train), 1))
y_train = np.array(y_train)

model = tf.keras.Sequential([
    Embedding(5, 2),#对输入数据进行Embedding编码,这一层会生成一个5行2列的可训练参数矩阵
    SimpleRNN(3),
    Dense(5, activation='softmax')
])

最后测试时,代码改动:
preNum = int(input("input the number of test alphabet:"))
for i in range(preNum):
    alphabet1 = input("input test alphabet:")
    #########################
    alphabet = [w_to_id[alphabet1]]
    # 使alphabet符合Embedding输入要求:[送入样本数, 循环核时间展开步数]。
    # 此处验证效果送入了1个样本,送入样本数为1;输入1个字母出结果,循环核时间展开步数为1。
    alphabet = np.reshape(alphabet, (1, 1))
    ##########################
    result = model.predict(alphabet)
    pred = tf.argmax(result, axis=1)
    pred = int(pred)
    tf.print(alphabet1 + '->' + input_word[pred])

连续输入26个字母的任意四个字母预测下一个

代码:

import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import Dense, SimpleRNN, Embedding
import matplotlib.pyplot as plt
import os

input_word = "abcdefghijklmnopqrstuvwxyz"
w_to_id = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4,
           'f': 5, 'g': 6, 'h': 7, 'i': 8, 'j': 9,
           'k': 10, 'l': 11, 'm': 12, 'n': 13, 'o': 14,
           'p': 15, 'q': 16, 'r': 17, 's': 18, 't': 19,
           'u': 20, 'v': 21, 'w': 22, 'x': 23, 'y': 24, 'z': 25}  # 单词映射到数值id的词典

training_set_scaled = [0, 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]

x_train = []
y_train = []

for i in range(4, 26):
    x_train.append(training_set_scaled[i - 4:i])
    y_train.append(training_set_scaled[i])

np.random.seed(7)
np.random.shuffle(x_train)
np.random.seed(7)
np.random.shuffle(y_train)
tf.random.set_seed(7)

# 使x_train符合Embedding输入要求:[送入样本数, 循环核时间展开步数] ,
# 此处整个数据集送入所以送入,送入样本数为len(x_train);输入4个字母出结果,循环核时间展开步数为4。
x_train = np.reshape(x_train, (len(x_train), 4))
y_train = np.array(y_train)

model = tf.keras.Sequential([
    Embedding(26, 2),
    SimpleRNN(10),
    Dense(26, activation='softmax')
])

model.compile(optimizer=tf.keras.optimizers.Adam(0.01),
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
              metrics=['sparse_categorical_accuracy'])

checkpoint_save_path = "./checkpoint/rnn_embedding_4pre1.ckpt"

if os.path.exists(checkpoint_save_path + '.index'):
    print('-------------load the model-----------------')
    model.load_weights(checkpoint_save_path)

cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path,
                                                 save_weights_only=True,
                                                 save_best_only=True,
                                                 monitor='loss')  # 由于fit没有给出测试集,不计算测试集准确率,根据loss,保存最优模型

history = model.fit(x_train, y_train, batch_size=32, epochs=100, callbacks=[cp_callback])

model.summary()

file = open('./weights.txt', 'w')  # 参数提取
for v in model.trainable_variables:
    file.write(str(v.name) + '\n')
    file.write(str(v.shape) + '\n')
    file.write(str(v.numpy()) + '\n')
file.close()

###############################################    show   ###############################################

# 显示训练集和验证集的acc和loss曲线
acc = history.history['sparse_categorical_accuracy']
loss = history.history['loss']

plt.subplot(1, 2, 1)
plt.plot(acc, label='Training Accuracy')
plt.title('Training Accuracy')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(loss, label='Training Loss')
plt.title('Training Loss')
plt.legend()
plt.show()

################# predict ##################

preNum = int(input("input the number of test alphabet:"))
for i in range(preNum):
    alphabet1 = input("input test alphabet:")
    alphabet = [w_to_id[a] for a in alphabet1]
    # 使alphabet符合Embedding输入要求:[送入样本数, 时间展开步数]。
    # 此处验证效果送入了1个样本,送入样本数为1;输入4个字母出结果,循环核时间展开步数为4。
    alphabet = np.reshape(alphabet, (1, 4))
    result = model.predict([alphabet])
    pred = tf.argmax(result, axis=1)
    pred = int(pred)
    tf.print(alphabet1 + '->' + input_word[pred])

RNN实现股票预测

在保存好的csv文件中对贵州茅台股票进行预测,使用前60天的开盘价预测第61天的开盘价。

整体代码:

import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import Dropout, Dense, SimpleRNN
import matplotlib.pyplot as plt
import os
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error
import math
#1.读取数据集
maotai = pd.read_csv('./SH600519.csv')  # 读取股票文件
#2.切分数据集
training_set = maotai.iloc[0:2426 - 300, 2:3].values  # 前(2426-300=2126)天的开盘价作为训练集,表格从0开始计数,2:3 是提取[2:3)列,前闭后开,故提取出C列开盘价
test_set = maotai.iloc[2426 - 300:, 2:3].values  # 后300天的开盘价作为测试集
#3.数据处理
# 归一化,使送入神经网络的数据分布在0到1之间
sc = MinMaxScaler(feature_range=(0, 1))  # 定义归一化:归一化到(0,1)之间
training_set_scaled = sc.fit_transform(training_set)  # 求得训练集的最大值,最小值这些训练集固有的属性,并在训练集上进行归一化
test_set = sc.transform(test_set)  # 利用训练集的属性对测试集进行归一化
#建立空列表分别用于接收训练集输入特征与标签
x_train = []
y_train = []
#建立空列表分别用于接收测试集输入特征与标签
x_test = []
y_test = []
#4.生成训练集与测试集
# 测试集:csv表格中前2426-300=2126天数据
# 利用for循环,遍历整个训练集,提取训练集中连续60天的开盘价作为输入特征x_train,第61天的数据作为标签,for循环共构建2426-300-60=2066组数据。
for i in range(60, len(training_set_scaled)):
    x_train.append(training_set_scaled[i - 60:i, 0])
    y_train.append(training_set_scaled[i, 0])
# 对训练集进行打乱
np.random.seed(7)
np.random.shuffle(x_train)
np.random.seed(7)
np.random.shuffle(y_train)
tf.random.set_seed(7)
# 将训练集由list格式变为array格式
x_train, y_train = np.array(x_train), np.array(y_train)

# 使x_train符合RNN输入要求:[送入样本数, 循环核时间展开步数, 每个时间步输入特征个数]。
# 此处整个数据集送入,送入样本数为x_train.shape[0]即2066组数据;输入60个开盘价,预测出第61天的开盘价,循环核时间展开步数为60; 每个时间步送入的特征是某一天的开盘价,只有1个数据,故每个时间步输入特征个数为1
x_train = np.reshape(x_train, (x_train.shape[0], 60, 1))
# 测试集:csv表格中后300天数据
# 利用for循环,遍历整个测试集,提取测试集中连续60天的开盘价作为输入特征x_train,第61天的数据作为标签,for循环共构建300-60=240组数据。
for i in range(60, len(test_set)):
    x_test.append(test_set[i - 60:i, 0])
    y_test.append(test_set[i, 0])
# 测试集变array并reshape为符合RNN输入要求:[送入样本数, 循环核时间展开步数, 每个时间步输入特征个数]
x_test, y_test = np.array(x_test), np.array(y_test)
x_test = np.reshape(x_test, (x_test.shape[0], 60, 1))#转变成RNN接收的格式

#5.使用Sequential搭建神经网络
model = tf.keras.Sequential([
    SimpleRNN(80, return_sequences=True),#第一层循环计算层记忆体设定80个,每个时间步推送记忆体ht给下一层
    Dropout(0.2),
    SimpleRNN(100),#第一层循环计算层记忆体设定100个
    Dropout(0.2),
    Dense(1)#由于输出值是第61一天的开盘价,只有一个数,所以Dense是1
])
#6.compile配置训练方法
model.compile(optimizer=tf.keras.optimizers.Adam(0.001),#adam优化器
              loss='mean_squared_error')  # 损失函数用均方误差
# 该应用只观测loss数值,不观测准确率,所以删去metrics选项,一会在每个epoch迭代显示时只显示loss值

#7.设置断点续训
checkpoint_save_path = "./checkpoint/rnn_stock.ckpt"

if os.path.exists(checkpoint_save_path + '.index'):
    print('-------------load the model-----------------')
    model.load_weights(checkpoint_save_path)
#fit的断点续训回调函数
cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path,
                                                 save_weights_only=True,
                                                 save_best_only=True,
                                                 monitor='val_loss')
#8.执行训练过程
history = model.fit(x_train, y_train, batch_size=64, epochs=50, validation_data=(x_test, y_test), validation_freq=1,
                    callbacks=[cp_callback])
#9.打印出网络结构
model.summary()
#10.参数提取
file = open('./weights.txt', 'w')  # 参数提取
for v in model.trainable_variables:
    file.write(str(v.name) + '\n')
    file.write(str(v.shape) + '\n')
    file.write(str(v.numpy()) + '\n')
file.close()
#11.loss可视化
loss = history.history['loss']
val_loss = history.history['val_loss']

plt.plot(loss, label='Training Loss')
plt.plot(val_loss, label='Validation Loss')
plt.title('Training and Validation Loss')
plt.legend()
plt.show()


#12.用predict预测测试集数据,将预测值和真实值从归一化的数值转换到真实数值
################## predict ######################
# 测试集输入模型进行预测
predicted_stock_price = model.predict(x_test)
# 对预测数据还原---从(0,1)反归一化到原始范围
predicted_stock_price = sc.inverse_transform(predicted_stock_price)
# 对真实数据还原---从(0,1)反归一化到原始范围
real_stock_price = sc.inverse_transform(test_set[60:])
# 画出真实数据和预测数据的对比曲线
plt.plot(real_stock_price, color='red', label='MaoTai Stock Price')
plt.plot(predicted_stock_price, color='blue', label='Predicted MaoTai Stock Price')
plt.title('MaoTai Stock Price Prediction')
plt.xlabel('Time')
plt.ylabel('MaoTai Stock Price')
plt.legend()
plt.show()


#13.评价模型优劣
##########evaluate##############
# calculate MSE 均方误差 ---> E[(预测值-真实值)^2] (预测值减真实值求平方后求均值)
mse = mean_squared_error(predicted_stock_price, real_stock_price)
# calculate RMSE 均方根误差--->sqrt[MSE]    (对均方误差开方)
rmse = math.sqrt(mean_squared_error(predicted_stock_price, real_stock_price))
# calculate MAE 平均绝对误差----->E[|预测值-真实值|](预测值减真实值求绝对值后求均值)
mae = mean_absolute_error(predicted_stock_price, real_stock_price)
print('均方误差: %.6f' % mse)
print('均方根误差: %.6f' % rmse)
print('平均绝对误差: %.6f' % mae)

使用LSTM实现股票预测

之前讲的传统循环神经网络RNN可以通过记忆体实现短期记忆进行连续数据的预测,但是当连续数据的序列变长时,会使展开时间步过长,在反向传播更新参数时,梯度要按照时间步连续相乘,会导致梯度消失,所以提出了LSTM长短记忆网络

长短记忆网络提出了三个记忆门限,引入了细胞态Ct,引入了等待存入长期记忆的候选态Ct波浪号。

具体看代码p47

使用GRU实现股票预测

是对LSTM的简化结构,GRU使记忆体ht融合了长期记忆和短期记忆,记忆体ht包含了过去信息ht-1和现在信息ht波浪号,现在信息ht波浪号是由过去信息ht-1过重置门与当前输入共同决定的。

具体代码看p48

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值