【tensorflow】4.卷积神经网络详解_MINIST实例

2024-08-28 23:08

本文主要是介绍【tensorflow】4.卷积神经网络详解_MINIST实例,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文中,会搭建一个简单的卷积网络,实现手写体数据集MINIST的识别。
通过本文,可以学到卷积神经网络的一般结构,体会数据的整个流动,重点是体会数据维度的对应关系,理解各层的作用和参数的意义。


2018.5.29更新:重新组织代码。增加测试,采用真实的手写图片进行测试!

采用真实数据做测试,一定要符合MINIST数据集的要求

  • label是[0,1,2,3,4,5,6,7,8,9]的顺序标号。
  • image数据必须归一化到[0,1]的阈值化图像

一. 卷积神经网络

所谓卷积神经网络,即神经元与输入数据经行的运算不是简单的线性运算。一般的神经网络中,隐层的神经元于输入进行的是以下操作:

y=wX+b,f(y)=Y y = w ∗ X + b , f ( y ) = Y

此处 f(y) f ( y ) 是一个非线性映射,如sigmod函数。如下图

这里写图片描述

而卷积神经网络的神经元,与输入进行的是卷积操作(表面理解就是按模板权值进行的乘加操作,实际上就是一个有关空间的局部线性叠加)。如下图所示(该图中,边缘没有填充(采用valid策略),所以尺寸缩小了,若填充边缘,则卷积不会改变尺寸)(注意‘尺度’和‘尺寸’的区别

这里写图片描述

由于卷积利用了空间特性,故通常在卷积的时候,需要保留原空间信息——即图片最好不要是向量形式,而是原始的矩阵形式

卷积层,或者说卷积操作有何意义?

在图像处理中,对图像的滤波通常采用的就是卷积操作,如一个 3×3 3 × 3 的均值滤波器卷积核(kernel)为{{1,1,1},{1,1,1},{1,1,1}},该kernel的作用是均值平滑去噪。而一个laplace kernel可以用来提取图像的边缘信息!其模板如下

这里写图片描述

所以我们认为卷积层对图像进行一系列的卷积操作,实际上是通过网络学习到的kernel对图像进行信息的提取(比如,若学习到的模板参数正好是laplace模板系数,那么这个neuron进行的就是边缘提取功能)。


网络结构

当然卷积神经网络中,并不是只有卷积层,这里我们来了解以下其他的网络层结构,及其作用。

把握CNN结构,最重要的是掌握样本数据在网络中的流动,维度的变化,尺寸的变化。

下面以本例代码为例,讲解本例的网络结构。网络总体结构如下

这里写图片描述

1. 输入层

维度: n×784 n × 784

这里n表示的样本数,784是一张图片的 width×height w i d t h × h e i g h t ,所得得总像素数目。即图像得数据向量。但是由于卷积利用得是原始图像矩阵,故需要一个reshape操作,将图片reshape成原始得 28×28×1 28 × 28 × 1 的形式。

这个1需要重点理解以下,它表示数据的深度,在输入数据上,表示的是图像的通道,通常彩色图像是3或者4(多了一个透明度alpha)通道(channel),而本文所用的图片都是灰度图,只有一个通道,即深度为1。如下图所示

这里写图片描述

故我们将reshape操作后的数据看作输入层的输出(怪怪的,懂这个意思就是了,反正就是进入卷积网络的数据)

维度: n×28×28×1 n × 28 × 28 × 1

之后的层中我们将忽略这个n(这个n通常是一个batch_size,因为送入网络的通常是一批样本数据,而不是整个训练集),只关注一个样本在网络中的流动。

2.第一个卷积层conv1

  • input: 1×28×28×1 1 × 28 × 28 × 1 ,size是 28×28 28 × 28

  • kernel:卷积模板, 3×3×1 3 × 3 × 1 ,分别是长,宽,深(depth,channel)

  • neuron:神经元个数,64个。此处需要理解,一个神经元对图像进行一趟完整的卷积(即卷积模板扫过图像的所有像素),得到1个卷积结果图(结果也是矩阵)。

  • out:由neuron得出,输出是64个卷积结果图(称之为特征图,feature map)

  • parameter:上述分析,可得conv1的卷积参数 28×28×1×64 28 × 28 × 1 × 64

  • strides:卷积过程中,描述模板窗口滑动的参数,通常设置为strides = [1, stride, stride, 1],stride为水平和垂直的kernel滑动步长。

  • padding:卷积操作会遇到边缘问题(模板移出图像边界),此处需要添加边缘(在外层套一层像素点),增加像素点的策略,内置了两种,’SAME’,表示边缘填充0,’VALID’,表示边缘如果不够一次卷积操作,就是说需要增加边缘像素那么就放弃这一次卷积,进入下一行进行卷积操作。

  • out_size:这里指单个neuron得到的卷积图像的尺寸,通过input_size 、kernel_size、strides计算得 28×28 28 × 28 ,由于padding设置了添加’SAME’,会进行边缘填充,故尺寸不会变化。

  • code:w_conv1 = tf.Variable(tf.random_normal([3, 3, 1, 64], stddev=std))

    conv1 = tf.nn.conv2d(_x, w_conv1, strides=[1, 1, 1, 1], padding='SAME')

这里写图片描述

3.第一个池化层

通常,一个卷积层之后会接一个池化层,池化的理解就是下采样(downsample),个人理解池化有以下作用

  • 降低数据规模
  • 模拟认知的抽象,即精细的高分辨率细节向高层模糊,然后通过后续卷积提取高层语义特征。
  • 实现尺度变换,让各卷积层提取的特征存在尺度上的差异。(这一点在FPN中被用到,但FPN还存在top-down的连接暂且不说)

事实上,卷积层也具有上述两个功能(尺寸是否变化得看padding策略,通常为了方便计算尺寸参数,建议采用’SAME’策略),但不同的是,卷积层通过卷积(线性叠加)操作具有了提取特征的能力,可以理解为pooling是为了卷积层服务的。Pooling的图示如下(显然池化层会改变尺寸!

这里写图片描述

常见的池化方式(采样方式)

  • 均值采样,mean-pooling,计算图像被kernel覆盖区域的平均值作为该区域池化后对应的一个像素的值。
  • 最大值采用,max-pooling,计算图像被kernel覆盖区域的最大值作为该区域池化后对应的一个像素的值。

本例中的池化层1,pool1

  • input: 1×28×28×64 1 × 28 × 28 × 64
  • policy:max-pooling
  • parameter:ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME',即采样是4个点取一个最大值点,步长也是[2,2],说明kernel不会重叠,padding采用边缘超出时补0的策略
  • out: 1×14×14×64 1 × 14 × 14 × 64
  • size:从 28×28 28 × 28 变到 14×14 14 × 14

4. 第二个卷积层

  • input: 1×14×14×64 1 × 14 × 14 × 64
  • neuron:128
  • out: 1×14×14×128 1 × 14 × 14 × 128 ,得到深度(也称为channel)为128的特征图
  • size: 14×14 14 × 14 ,不变

5.第二个池化层

  • input: 1×14×14×128 1 × 14 × 14 × 128
  • policy:max-pooling
  • parameter:ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME'
  • out: 1×7×7×128 1 × 7 × 7 × 128
  • size:从 14×14 14 × 14 变到 7×7 7 × 7

至此,通过两个卷积层(包括其后接的pooling和dropout),从原图得到了深度为128的 7×7 7 × 7 的特征图,之后需要将这个所得的特征图,reshape成特征向量,然后进行分类任务,做分类任务的时候,可以用神经网络方法,也可以用其他机器学习方法,本例用的神经网络算法,故reshape之后接了两层全连接层(这两层全连接层各有作用,下文说明)。

1*7*7*128 reshape 1*6272*1 #这里第一个维度表示的样本数,此出为1,表示一个样本
feature_vector = tf.reshape(conv2_out, [-1,_w['w_fc1'].get_shape().as_list()[0]])

6.第一层全连接

深层网络和浅层网络相比有什么优势呢?
简单来说深层网络能够表达力更强。事实上,一个仅有一个隐藏层的神经网络就能拟合任何一个函数,但是它需要很多很多的神经元。而深层网络用少得多的神经元就能拟合同样的函数。也就是为了拟合一个函数,要么使用一个浅而宽的网络,要么使用一个深而窄的网络。而后者往往更节约资源。
但深层网络的缺点是,不太容易训练。

经验上,相比于浅层但多neuron网络,我们更倾向于使用少的neuron的多层网络。

上述理论和这里的第一个连接层的作用并没有特别大的关系,但是还是想提一下。本例中的第一层全连接什么作用呢?

我认为,最主要的作用就是降维:图像得到的特征向量有6272维,这里我们通过第一层全连接,降到1024维。

  • input: 1×6272×1 1 × 6272 × 1
  • neuron:1024个,全连接神经元,即和所有的输入数据按权值相连。
  • out: 1×1024×1 1 × 1024 × 1
  • code:w_fc1 = tf.Variable(tf.random_normal([7 * 7 * 128, 1024], stddev=std))
    fc1 = tf.nn.relu(tf.add(tf.matmul(feature_vector, w_fc1, b_fc1)) # relu激活函数

7.第二层全连接

这一层全连接,直接输出分类结果,可以在后面添加softmax层,输出个分类的得分(概率),然后loss 使用logistic loss,当然,这等效于不加softmax,但后续的cost function用softmax_cross_entropy_with_logits,代码如下(事实上,logistic loss 和 cross entropy loss 殊途同归)

# 代码引用自:http://blog.csdn.net/caimouse      
import tensorflow as tf    #our NN's output    
logits = tf.constant([[1.0,2.0,3.0],[1.0,2.0,3.0],[1.0,2.0,3.0]])  #step1:do softmax    
y = tf.nn.softmax(logits)  #true label    
y_ = tf.constant([[0.0,0.0,1.0],[0.0,0.0,1.0],[0.0,0.0,1.0]])    
#step2:do cross_entropy    
cross_entropy = -tf.reduce_sum(y_*tf.log(y))    
#do cross_entropy just one step    
cross_entropy2 = tf.reduce_sum(tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=y_))  with tf.Session() as sess:    softmax=sess.run(y)    c_e = sess.run(cross_entropy)    c_e2 = sess.run(cross_entropy2)    print("step1:softmax result=")    print(softmax)    print("step2:cross_entropy result=")    print(c_e)    print("Function(softmax_cross_entropy_with_logits) result=")    print(c_e2) # 输出
'''
step1:softmax result=
[[ 0.09003057  0.24472848  0.66524094][ 0.09003057  0.24472848  0.66524094][ 0.09003057  0.24472848  0.66524094]]
step2:cross_entropy result=
1.22282
Function(softmax_cross_entropy_with_logits) result=
1.22282
'''

上述代码来自:caimouse的博客

本例的第二层全连接用于输出分类,具体细节如下

  • input: 1×1024×1 1 × 1024 × 1
  • neuron:10个,全连接神经元,输出对应到10个分类的得分。
  • out: 1×10×1 1 × 10 × 1
  • code:w_fc2 = tf.Variable(tf.random_normal([1024, 10], stddev=std))
    out = tf.add(tf.matmul(fc1_out, w_fc2), b_fc2) # 输出层不用激活

至此,本例的网络结构分析完毕,下面上代码

# -*- coding: utf-8 -*-
# @File    : 5_CNN_MINIST.py
# @Time    : 2018/5/29 20:49
# @Author  : hyfine
# @Contact : foreverfruit@126.com
# @Desc    : 建立简单的卷积神经网络结构,实现手写体的识别import input_data
import cv2
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt# 数据集合
def load_dataset():print('------------ DATA LOADING ------------')# minist数据集采用tensorflow自带的加载脚本input_data.py加载minist = input_data.read_data_sets('data/', one_hot=True)# 训练集55000,验证集5000,测试集10000# 模型训练的时候,验证集全部输入,我显卡会OOM,所以只用了前1000张做准确率的测试# 测试的时候,没有采用数据集自带的sample,而是自己预处理了手写的数据用于测试print("train shape:", minist.train.images.shape, minist.train.labels.shape)print("test  shape:", minist.test.images.shape, minist.test.labels.shape)print("valid shape:", minist.validation.images.shape, minist.validation.labels.shape)print("----------MNIST loaded----------------")return {'train': minist.train, 'test': minist.test, 'valid': minist.validation}# 定义网络结构,预定义网络参数
def net_params():# 网络中的参数(输入,输出,权重,偏置)n_input = 784n_out = 10# 标准差std = 0.1w = {'w_conv1': tf.Variable(tf.random_normal([3, 3, 1, 64], stddev=std)),'w_conv2': tf.Variable(tf.random_normal([3, 3, 64, 128], stddev=std)),# 此处的7*7是前两层卷积池化之后的尺寸,是通过网络结构参数,计算出来的'w_fc1': tf.Variable(tf.random_normal([7 * 7 * 128, 1024], stddev=std)),'w_fc2': tf.Variable(tf.random_normal([1024, n_out], stddev=std))}b = {'b_conv1': tf.Variable(tf.zeros([64])),'b_conv2': tf.Variable(tf.zeros([128])),'b_fc1': tf.Variable(tf.zeros([1024])),'b_fc2': tf.Variable([tf.zeros([n_out])])}print('-------------- CNN_NET READY! --------------')return {'input_len': n_input, 'output_len': n_out, 'weight': w, 'bias': b}# 前向传播过程。(各网络层的连接)
def forward_propagation(_x, _w, _b, _keep_ratio):# 1.将输入x向量矩阵化,因为卷积要在图像矩阵上操作_x = tf.reshape(_x, [-1, 28, 28, 1])# 2.第一个卷积+池化+dropoutconv1 = tf.nn.conv2d(_x, _w['w_conv1'], strides=[1, 1, 1, 1], padding='SAME')# 输出归一化(保证下一层的输入数据是经过归一化的)# _mean, _var = tf.nn.moments(conv1, [0, 1, 2])# conv1 = tf.nn.batch_normalization(conv1, _mean, _var, 0, 1, 0.0001)# 激活函数,activationconv1 = tf.nn.relu(tf.nn.bias_add(conv1, _b['b_conv1']))# 推荐数据流动过程中查看shape变化情况print('conv1:', conv1.shape)# 池化pool1 = tf.nn.max_pool(conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')print('pool1:', pool1.shape)# 失活,dropoutout_conv1 = tf.nn.dropout(pool1, _keep_ratio)# 3.第二个卷积+池化+dropoutconv2 = tf.nn.conv2d(out_conv1, _w['w_conv2'], strides=[1, 1, 1, 1], padding='SAME')# _mean, _var = tf.nn.moments(conv1, [0, 1, 2])# conv1 = tf.nn.batch_normalization(conv1, _mean, _var, 0, 1, 0.0001)# 激活函数,activationconv2 = tf.nn.relu(tf.nn.bias_add(conv2, _b['b_conv2']))print('conv2:', conv2.shape)# 池化pool2 = tf.nn.max_pool(conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')print('pool2:', pool2.shape)# 失活,dropoutout_conv2 = tf.nn.dropout(pool2, _keep_ratio)# 4.向量化,之后的全连接的输入应该是一个样本的特征向量feature_vector = tf.reshape(out_conv2, [-1, _w['w_fc1'].get_shape().as_list()[0]])print('feature_vector:', feature_vector.shape)# 5.第一个 full connected layer,特征向量降维fc1 = tf.nn.relu(tf.add(tf.matmul(feature_vector, _w['w_fc1']), _b['b_fc1']))fc1_do = tf.nn.dropout(fc1, _keep_ratio)print('fc1:', fc1.shape)# 6.第二个 full connected layer,分类器out = tf.add(tf.matmul(fc1_do, _w['w_fc2']), _b['b_fc2'])print('fc2:', out.shape)return out# 训练过程
def training(train, valid):# 先得到网络参数params = net_params()n_input = params['input_len']n_out = params['output_len']w = params['weight']b = params['bias']keep_ratio = tf.placeholder(tf.float32)# 输入数据,及其对应的真实labels,这里用placeholder占位x = tf.placeholder(tf.float32, [None, n_input])y = tf.placeholder(tf.float32, [None, n_out])y_ = forward_propagation(x, w, b, keep_ratio)# 训练任务:优化求解最小化costcost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(logits=y_, labels=y))lr = 0.01optm = tf.train.AdamOptimizer(lr).minimize(cost)# 计算准确率,用以衡量模型result_bool = tf.equal(tf.argmax(y_, 1), tf.argmax(y, 1))accuracy = tf.reduce_mean(tf.cast(result_bool, tf.float32))init = tf.global_variables_initializer()sess = tf.Session()sess.run(init)print('------- Start Training! ----------')# 训练参数epochs = 20display_step = 1# 本人是cpu版本tensorflow,全部样本训练太慢,这里选用部分数据,且batch_size也较小batch_size = 200# batch_count = 100batch_count = int(train.num_examples / batch_size)for epoch in range(epochs):avg_cost = 0.batch_x, batch_y = None, None# 分批次进行最小化loss的训练过程for batch_index in range(batch_count):batch_x, batch_y = train.next_batch(batch_size)feeds = {x: batch_x, y: batch_y, keep_ratio: 0.6}sess.run(optm, feed_dict=feeds)avg_cost += sess.run(cost, feed_dict=feeds)avg_cost /= batch_count# 检查当前模型的准确率,查看拟合情况if epoch % display_step == display_step - 1:feed_train = {x: batch_x, y: batch_y, keep_ratio: 1}# 全部的验证集都会OOM,所以只取前1000张feed_valid = {x: valid.images[:1000], y: valid.labels[:1000], keep_ratio: 1}ac_train = sess.run(accuracy, feed_dict=feed_train)ac_valid = sess.run(accuracy, feed_dict=feed_valid)print('Epoch: %03d/%03d cost: %.5f train_accuray:%0.5f valid_accuray:%0.5f' % (epoch + 1, epochs, avg_cost, ac_train, ac_valid))print('------- TRAINING COMPLETE ------------')# 保存模型saver = tf.train.Saver()# 这里后缀名ckpt,表示checkpoint,这个可以任意save_path = saver.save(sess, 'model/cnn_mnist_model.ckpt')print(save_path)print('------ MODEL SAVED --------------')def image_preprocess(file_path: str):"""读取图片并返回图片的预处理之后的数据,预处理包括(resize、reshape、threshold):param file_path: 图片的地址:return: numpy.ndarray类型的数据预处理之后的数据"""image = cv2.imread(file_path, cv2.IMREAD_GRAYSCALE)# 默认双线性插值resizeimage = cv2.resize(image, (28, 28))# 二值化ret, image = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY_INV)# 为了强化图像,再进行腐蚀和膨胀操作(这里因为经过反向二值化,应该做闭操作)# kernel = np.ones((5, 5), np.uint8)# image = cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel)# 显示图片(查看二值效果)# plt.imshow(image)# plt.show()# 归一化image = image / 255.image = image.astype(np.float32)return np.reshape(image, [1, 784])def test():# 先得到网络参数params = net_params()n_input = params['input_len']n_out = params['output_len']w = params['weight']b = params['bias']keep_ratio = tf.placeholder(tf.float32)x = tf.placeholder(tf.float32, [None, n_input])y = tf.placeholder(tf.float32, [None, n_out])y_ = forward_propagation(x, w, b, keep_ratio)# 求具体的分类结果result = tf.argmax(tf.nn.softmax(y_), 1)init = tf.global_variables_initializer()sess = tf.Session()sess.run(init)# 加载模型saver = tf.train.Saver()saver.restore(sess, 'model/cnn_mnist_model.ckpt')print('---------- Model restored! ------------')# 进行测试print('---------- TESTING ---------------------')# 输入图像img_path = 'data/mini_test_2_original.png'test_2 = image_preprocess(img_path)img_path = 'data/mini_test_3_original.png'test_3 = image_preprocess(img_path)img_path = 'data/mini_test_7_original.png'test_7 = image_preprocess(img_path)img_path = 'data/mini_test_8.png'test_8 = image_preprocess(img_path)# 组成输入矩阵test_input = np.vstack((test_2, test_3, test_7, test_8))# 输入网络计算feed_test = {x: test_input, keep_ratio: 1}result = sess.run(result, feed_dict=feed_test)print('------------ 测试结果 --------------')for i, v in enumerate(result):print('第%d个输入的是:%d' % (i + 1, v))# 查看具体的得分情况print(sess.run(y_, feed_dict=feed_test))if __name__ == '__main__':#data_set = load_dataset()#training(data_set['train'], data_set['valid'])test()

输出结果

这里写图片描述

这里写图片描述

这篇关于【tensorflow】4.卷积神经网络详解_MINIST实例的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1116147

相关文章

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

mac中资源库在哪? macOS资源库文件夹详解

《mac中资源库在哪?macOS资源库文件夹详解》经常使用Mac电脑的用户会发现,找不到Mac电脑的资源库,我们怎么打开资源库并使用呢?下面我们就来看看macOS资源库文件夹详解... 在 MACOS 系统中,「资源库」文件夹是用来存放操作系统和 App 设置的核心位置。虽然平时我们很少直接跟它打交道,但了

关于Maven中pom.xml文件配置详解

《关于Maven中pom.xml文件配置详解》pom.xml是Maven项目的核心配置文件,它描述了项目的结构、依赖关系、构建配置等信息,通过合理配置pom.xml,可以提高项目的可维护性和构建效率... 目录1. POM文件的基本结构1.1 项目基本信息2. 项目属性2.1 引用属性3. 项目依赖4. 构

Rust 数据类型详解

《Rust数据类型详解》本文介绍了Rust编程语言中的标量类型和复合类型,标量类型包括整数、浮点数、布尔和字符,而复合类型则包括元组和数组,标量类型用于表示单个值,具有不同的表示和范围,本文介绍的非... 目录一、标量类型(Scalar Types)1. 整数类型(Integer Types)1.1 整数字

Java操作ElasticSearch的实例详解

《Java操作ElasticSearch的实例详解》Elasticsearch是一个分布式的搜索和分析引擎,广泛用于全文搜索、日志分析等场景,本文将介绍如何在Java应用中使用Elastics... 目录简介环境准备1. 安装 Elasticsearch2. 添加依赖连接 Elasticsearch1. 创

使用C#代码计算数学表达式实例

《使用C#代码计算数学表达式实例》这段文字主要讲述了如何使用C#语言来计算数学表达式,该程序通过使用Dictionary保存变量,定义了运算符优先级,并实现了EvaluateExpression方法来... 目录C#代码计算数学表达式该方法很长,因此我将分段描述下面的代码片段显示了下一步以下代码显示该方法如

Redis缓存问题与缓存更新机制详解

《Redis缓存问题与缓存更新机制详解》本文主要介绍了缓存问题及其解决方案,包括缓存穿透、缓存击穿、缓存雪崩等问题的成因以及相应的预防和解决方法,同时,还详细探讨了缓存更新机制,包括不同情况下的缓存更... 目录一、缓存问题1.1 缓存穿透1.1.1 问题来源1.1.2 解决方案1.2 缓存击穿1.2.1

PyTorch使用教程之Tensor包详解

《PyTorch使用教程之Tensor包详解》这篇文章介绍了PyTorch中的张量(Tensor)数据结构,包括张量的数据类型、初始化、常用操作、属性等,张量是PyTorch框架中的核心数据结构,支持... 目录1、张量Tensor2、数据类型3、初始化(构造张量)4、常用操作5、常用属性5.1 存储(st

Python 中 requests 与 aiohttp 在实际项目中的选择策略详解

《Python中requests与aiohttp在实际项目中的选择策略详解》本文主要介绍了Python爬虫开发中常用的两个库requests和aiohttp的使用方法及其区别,通过实际项目案... 目录一、requests 库二、aiohttp 库三、requests 和 aiohttp 的比较四、requ

VUE动态绑定class类的三种常用方式及适用场景详解

《VUE动态绑定class类的三种常用方式及适用场景详解》文章介绍了在实际开发中动态绑定class的三种常见情况及其解决方案,包括根据不同的返回值渲染不同的class样式、给模块添加基础样式以及根据设... 目录前言1.动态选择class样式(对象添加:情景一)2.动态添加一个class样式(字符串添加:情