r语言中which的使用_在R中使用TensorFlow构建一个用于图像分类的卷积神经网络

在本章中,我们将介绍以下主题:

  • 下载并配置图像数据集;
  • 学习CNN分类器的架构;
  • 使用函数初始化权重和偏差;
  • 使用函数创建一个新的卷积层;
  • 使用函数扁平化密集连接层;
  • 定义占位符变量;
  • 创建第一个卷积层;
  • 创建第二个卷积层;
  • 扁平化第二个卷积层;
  • 创建第一个完全连接的层;
  • 将dropout应用于第一个完全连接层;
  • 创建第二个带有dropout的完全连接层;
  • 应用Softmax激活以获得预测类;
  • 定义用于优化的成本函数;
  • 执行梯度下降成本优化;
  • 在TensorFlow会话中执行图;
  • 评估测试数据的性能。

3.1 介绍

卷积神经网络(Convolution Neural Network,CNN)是一类深度学习神经网络,在建立基于图像识别和自然语言处理的分类模型方面发挥着重要作用。

 CNN遵循类似于LeNet的架构(LeNet主要用于识别数字、邮政编码等字符)。和人工神经网络相比,CNN有以三维空间(宽度、深度和高度)排列的神经元层。每层将二维图像转换成三维输入体积,然后使用神经元激活函数将其转换为三维输出体积。

从根本上,CNN是使用3种主要激活层类型构建的:卷积层ReLU、池化层和完全连接层。卷积层用于从(图像的)输入向量中提取特征(像素之间的空间关系),并在带有权重(和偏差)的点积运算后将它们存储以供进一步处理。

然后,在卷积之后,在操作中应用ReLU以引入非线性。

这是应用于每个卷积特征映射的逐个元素操作(例如阈值函数、Sigmoid和tanh)。然后,池化层(诸如求最大值、求均值和求总和之类的操作)是用来降低每个特征映射的维度,以确保信息损失最小。这种减小空间大小的操作被用于控制过度拟合,并增加网络对小的失真或变换的鲁棒性。然后将池化层的输出连接到传统的多层感知器(也称为完全连接层)。该感知器使用例如Softmax或SVM的激活函数来建立基于分类器的CNN模型。

本章将着重于在R中使用TensorFlow构建一个用于图像分类的卷积神经网络。虽然本章将为你提供一个典型的CNN概览,但是我们鼓励你根据自己的需要调整并修改参数。

3.2 下载并配置图像数据集

在本章中,我们将使用CIFAR-10数据集来构建用于图像分类的卷积神经网络。CIFAR-10数据集由60,000个32×32彩色的10个种类的图像组成,每个种类有6,000个图像。这些进一步被分为5个训练批次和1个测试批次,每个批次有10,000个图像。

测试批次刚好包含1,000个从每个种类随机选择的图像。训练批次包含随机顺序的剩余图像,但是某些训练批次可能包含来自一个类的图像多余另一个类的图像。在它们之间,训练批次刚好包含5,000个来自每个种类的图像。10个结果类为飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。这些种类是完全互斥的。另外,数据集的格式如下。

  • 第一列,10个类的标签:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。
  • 接下来的1024列:范围在0~255的红色像素。
  • 接下来的1024列:范围在0~255的绿色像素。
  • 接下来的1024列:范围在0~255的蓝色像素。

3.2.1 做好准备

首先,你需要在R中安装一些软件包,比如data.table和imager。

3.2.2 怎么做

1.启动R(使用Rstudio或Docker)并加载所需的软件包。

2.从http://www.cs.toronto.edu/~kriz/cifar.html手动下载数据集(二进制版本)或在R环境中使用以下函数下载数据。该函数将工作目录或下载的数据集的位置路径作为输入参数(data_dir):

# 下载二进制文件的函数download.cifar.data 

3.一旦下载并解压数据集,就在R环境中读取数据集作为训练和测试数据集。该函数将训练和测试批数据集的文件名(filenames)以及每批文件检索的图像数(num.images)作为输入参数。

#读取cifar数据的函数 read.cifar.data 

4.之前函数的结果是每张图及其标签的红色、绿色和蓝色像素数据框列表。然后,使用以下函数将数据扁平化为两个数据框列表(一个用于输入,另一个用于输出)。该函数有两个参数:输入变量列表(x_listdata)和输出变量列表(y_listdata)。

# 扁平化数据的函数flat_data 

5.一旦输入和输出列表以及训练和测试数据框列表准备就绪,就通过绘制具有标签的图像来执行完整性检查。该函数需要两个必需的参数(index,图像的行编号;images.rgb,扁平化的输入数据集)和一个可选参数(images.lab,扁平化的输出数据集)。

labels 

6.现在使用最小-最大标准化(Min-Max Standardization)技术来转换输入数据。包中的preProcess函数可用于归一化。该方法的“range”选项执行最小-最大归一化(Min-Max Normalization),如下所示:

# 归一化数据的函数Require(caret)normalizeObj

3.2.3 工作原理

我们来看看在上一小节中做了什么。在3.2.2小节的步骤2中,我们从提到的链接中下载了CIFAR-10数据集,以防其不存在于给定的链接或工作目录中。在步骤3中,将解压缩的文件作为训练和测试数据集加载到R环境中。训练数据集有一个50,000张图像的列表,测试数据集有一个10,000张带标签的图像列表。然后,在步骤4中,训练和测试数据集被扁平化为两个数据框列表:一个长度为3,072(红色1,024、绿色1,024、蓝色1,024)的输入变量(或图像),一个长度为10(每个类的二进制)的输出变量(或标签)。在步骤5中,我们通过生成图对创建的训练和测试数据集进行完整性检查。图3-1(CIFAR-10数据集中的类别示列)显示了一组6个训练图像及其标签。最后,在步骤6中,使用最小-最大标准化技术来转换输入数据。

21080ed991f66f0d324ac7eb979b9e24.png

图3-1

3.3 学习CNN分类器的架构

本节介绍的CNN分类器有两个卷积层,最后两个为完全连接层,其中最后一层使用Softmax激活函数作为分类器。

3.3.1 做好准备

首先我们需要CIFAR-10数据集。因此,应该下载CIFAR-10数据集,并将其加载到R环境中。此外,图像的大小为32×32像素。

3.3.2 怎么做

我们来定义CNN分类器的配置,如下所示。

1.每个输入图像(CIFAR-10)的大小为32×32像素,可以标记为10个类别之一:

# CIFAR图像为32×32像素img_width = 32Limg_height = 32L# 使用图像高度和宽度的元组重新生成数组img_shape = c(img_width, img_height)# 类数量,每10个图像为一个类别num_classes = 10L

2.CIFAR-10数据集的图像有3个通道(红色,绿色和蓝色):

# 图像颜色通道的数量:红色、蓝色和绿色3个通道num_channels = 3L

3.图像存储在以下长度(img_size_flat)的一维数组中:

# 图像存储在一维数组中的长度img_size_flat = img_width * img_height * num_channels

4.在第一个卷积层中,卷积滤波器的大小(filter_size1)(宽×高)为5×5像素,卷积滤波器的深度(或数量,num_filters1)为64:

# 卷积层1filter_size1 = 5Lnum_filters1 = 64L

5.在第二卷积层中,卷积滤波器的大小和深度与第一卷积层相同:

# 卷积层2filter_size2 = 5Lnum_filters2 = 64L

6.类似地,第一完全连接层的输出与第二完全连接层的输入相同:

# 完全连接层fc_size = 1024L

3.3.3 工作原理

输入图像的尺寸和特征分别显示在3.3.2小节的步骤1和步骤2中。如3.3.2节中的步骤4和步骤5所定义的,每个输入图像在卷积层中使用一组滤波器进一步处理。第一个卷积层产生一组64张图像(每组滤波器一个)。 此外,这些图像的分辨率也减少到了一半(由于2×2最大池),即从32×32像素减少到16×16像素。

第二个卷积层将输入这64张图像,并提供进一步降低分辨率的新 64张图像的输出。更新后的分辨率现在是8×8像素(同样是由于2×2的最大池)。在第二卷积层中,总共创建了64×64=4,096个滤波器,然后将其进一步卷积成64个输出图像(或通道)。请记住,这64张8×8分辨率的图像对应单个输入图像。

进一步,如3.3.2小节中的步骤3所定义的,这些64张8×8像素的输出图像被扁平化为长度为4,096(8×8×64)的单向量,并被用作3.3.2小节的步骤6中所定义的给定的一组神经元的完全连接层的输入。然后将4,096个元素的向量反馈到1,024个神经元的第一完全连接层。输出神经元再次被反馈到10个神经元的第二完全连接层(等于num_classes)。这10个神经元代表每个类别标签,然后用于确定图像的最终类别。

首先,卷积和完全连接的层的权重被随机地进行初始化,直到分类阶段(CNN图的结尾)。此处,根据真实类别和预测类别(也称为交叉熵)来计算分类错误。

然后,优化器使用微分链式法则在卷积网络反向传播误差,之后更新层(或滤波器)的权重,使误差最小化。一个向前和向后传播的整个循环被称为一次迭代。执行数千次这样的迭代直到分类错误被降低到足够低的值。

 通常,使用一批图像而不是单个图像来执行这些迭代,以提高计算的效率。

图3-2描绘了本章设计的卷积网络。

da96a3c42baa1607ab6dc0f2e16dfc73.png

图3-2

3.4 使用函数初始化权重和偏差

权重和偏差是任何深度神经网络优化必不可少的组成部分,此处我们定义一些函数来自动执行这些初始化。以小噪声初始化权重来打破对称性并防止零梯度是很好的做法。此外,小的初始化正偏差将避免神经元失活,适合于ReLU激活神经元。

3.4.1 做好准备

权重和偏差是在模型编译之前需要初始化的模型系数。此步骤要求根据输入数据集确定shape参数。

3.4.2 怎么做

1.以下函数用于随机返回初始化权重:

# 权重初始化weight_variable 

2.以下函数用于返回常量偏差:

bias_variable 

3.4.3 工作原理

这些函数返回TensorFlow变量,稍后变量被用作TensorFlow图形的一部分。shape被定义为在卷积层中规定过滤器的属性列表,在下一节中将会介绍。权重随机初始化,标准偏差等于0.1,且偏差初始化为定值0.1。

3.5 使用函数创建一个新的卷积层

创建卷积层是CNN TensorFlow计算图中的主要步骤。该函数主要用于定义TensorFlow图形中的数学公式,之后在优化过程中用于实际计算。

3.5.1 做好准备

定义并加载输入数据集。本节中出现的create_conv_layer函数有以下5个输入参数,并需要在配置卷积层时定义。

1.input:这是四维张量(或者列表),包括多个(输入)图像、每张图的高度(此处为32L)、每张图的宽度(此处为32L)以及每张图通道的数量(此处为3L:红色、蓝色和绿色)。

2.num_input_channels:这被定义为在第一卷积层的情况下的颜色通道的数量或在随后的卷积层的情况下的过滤器通道的数量。

3.filter_size:这被定义为卷积层中每个过滤器的宽度和高度。此处,假定过滤器为正方形。

4.num_filters:这被定义为给定卷积层中过滤器的数量。

5.use_pooling:这是一个二进制变量,用于执行2×2最大池化。

3.5.2 怎么做

1.执行以下函数以创建一个新的卷积层:

# 创建一个新的卷积层create_conv_layer 

2.运行以下函数以生成卷积层图:

drawImage_conv 

3.运行以下函数以生成卷积层权重图:

drawImage_conv_weights 

3.5.3 工作原理

函数从创建形状张量开始,即过滤器的宽度、过滤器的高度、输入通道的数量和给定过滤器的数量这4个整数的列表。使用这种形状张量,用所定义的形状初始化一个新的权重张量,并为每个过滤器创建一个新的(常数)偏差。

一旦需要的权重和偏差被初始化,就使用tf$nn$conv2d函数为卷积创建一个TensorFlow操作。 在我们当前的配置中,所有4个维度的步长都设置为1,并且边距设置为相同(SAME)。第一个和最后一个默认设置为1,但中间的两个可以考虑更高的步长。步长是我们允许过滤器矩阵在输入(图像)矩阵上滑动的像素数量。

步长为3,将意味着每个过滤器片在x或y轴上有3个像素跳跃。较小的步长会产生较大的特征映射,因此需要较高的收敛计算。当内边距设置为SAME时,输入(图像)矩阵在边框周围填充零,以便我们可以将过滤器应用于输入矩阵的边框元素。使用此特征,我们可以控制输出矩阵(或特征映射)的大小与输入矩阵相同。

在卷积中,为每个跟随着池化的过滤器通道添加偏差数值,以防止过度拟合。在当前设置中,执行2×2最大池化(使用tf$nn$max_pool)来缩小图像分辨率。此处,我们考虑2×2(ksize)大小的窗口并选择每个窗口中的最大值。这些窗口在x或y方向上跨两个像素(步长)。

池化时,我们使用ReLU激活函数(tf$nn$relu)为层添加非线性。在ReLU中,在过滤器中触发每个像素,并且使用max(x,0)函数将所有负像素值替换为零,其中x是像素值。通常,在池化之前执行ReLU激活。但是,由于我们使用的是最大池化(Max-Pooling),因此它不一定会像这样影响结果,因为relu(max_pool(x))等同于max_pool(relu(x))。因此,通过在池化之后应用ReLU,我们可以节省大量的ReLU操作(~75%)。

最后,该函数返回一个卷积层及其相应权重的列表。卷积层是具有以下属性的4维张量:

  • (输入)图像的数量,同输入(input)一样;
  • 每张图的高度(在2×2最大池化的情况下减少到一半);
  • 每张图像的宽度(在2×2最大池化的情况下减少到一半);
  • 产生的通道数量,每个卷积过滤器一个。

3.6 使用函数创建一个扁平化的卷积层

新创建卷积层的四维结果被扁平化为二维层,以便它可以用作完全连接的多层感知器的输入。

3.6.1 做好准备

本小节解释如何在构建深度学习模型之前将卷积层扁平化。给定函数(flatten_conv_layer)的输入基于前一层定义的4维卷积层。

3.6.2 怎么做

运行以下函数以扁平化卷积层:

flatten_conv_layer 

3.6.3 工作原理

该函数从提取给定输入层的形状开始。如前面的章节中所述,输入层的形状由4个整数组成:图像编号,图像高度,图像宽度和图像中颜色通道的数量。然后使用图像高度、图像权重和颜色通道数量的点积来评估特征的数量(num_features)。

接着,将该层被扁平化或重塑为二维张量(使用tf$reshape):第一个维度设置为-1(等于图像总数),第二个维度是特征的数量。

最后,该函数返回一个扁平化的层列表以及特征(输入)总数量。

3.7 使用函数扁平化密集连接层

CNN通常以在输出层中使用Softmax激活的完全连接的多层感知器结束。此处,前一个卷积扁平化层中的每个神经元连接到下一个层(完全连接的)中的每个神经元。

 完全卷积层的关键目的是使用卷积和池化阶段生成的特征将给定的输入图像分类为各种结果类别(此处为10L)。它还有助于学习这些特征的非线性组合来定义结果类别。

在本章中,我们使用两个完全连接层进行优化。该函数主要用于定义TensorFlow图形中的数学公式,稍后在优化过程中用于实际计算。

3.7.1 做好准备

create_fc_layer函数有4个输入参数,如下所示。

  • input:与新的卷积层函数的输入类似。
  • num_inputs:扁平化卷积层后生成的输入特征的数量。
  • num_outputs:与输入神经元完全连接的输出神经元的数量。
  • use_relu:只有在最终完全连接层的情况下,才采用设置为错误(FALSE)的二进制标志。

3.7.2 怎么做

运行以下函数以创建一个新的完全连接层:

# 创建一个新的完全连接层create_fc_layer 

3.7.3 工作原理

函数create_fc_layer从初始化新的权重和偏差开始。然后,执行输入层与初始化权重的矩阵乘法,并添加相关的偏差。

如果完全连接层不是CNN TensorFlow图的最后一层,就可以执行ReLU非线性激活。最后,返回完全连接层。

3.8 定义占位符变量

在本节中,我们定义一个占位符变量,作为TensorFlow计算图中模块的输入。这些通常是张量形式的多维数组或矩阵。

3.8.1 做好准备

占位符变量的数据类型被设置为float32(tf$float32),并将形状设置为二维张量。

3.8.2 怎么做

1.创建一个输入占位符变量:

x = tf$placeholder(tf$float32, shape=shape(NULL, img_size_flat),name='x')

占位符中的NULL值允许我们传递不确定的数组大小。

2.将输入占位符x重塑为4维张量:

x_image = tf$reshape(x, shape(-1L, img_size, img_size,num_channels))

3.创建一个输出占位符变量:

y_true = tf$placeholder(tf$float32, shape=shape(NULL, num_classes),name='y_true')

4.使用argmax获取输出的类(true):

y_true_cls = tf$argmax(y_true, dimension=1L)

3.8.3 工作原理

在3.8.2小节的步骤1中,我们定义了一个输入占位符变量。形状张量的维度为NULL和img_size_flat。 将前者设置成可保存任意数量图像(作为行),后者定义每张图像输入特征的长度(作为列)。 在3.8.2小节的步骤2中,输入的二维张量被重塑为一个4维张量,可以作为输入卷积层。4个维度如下:

  • 第一个定义了输入图像的数量(当前设置为-1);
  • 第二个定义每张图的高度(相当于图像大小32L);
  • 第三个定义每张图的宽度(相当于图像大小,同样是32L);
  • 第四个定义每张图中的颜色通道数量(此处为3L)。

在3.8.2小节的步骤3中,我们定义一个输出占位符变量来保存x中图像的真实类或标签。形状张量的维度为NULL和num_classes。前者被设置为保存任意数量的图像(作为行),后者将每张图的真实类别定义为长度为num_classes的二进制向量(作为列)。在我们的场景中,有10类。在3.8.2小节的步骤4中,我们将二维输出占位符压缩为类别号从1到10的一维张量。

3.9 创建第一个卷积层

在本节中,我们来创建第一个卷积层。

3.9.1 做好准备

在“使用函数创建一个新的卷积层”一节(见3.5节)中定义了函数create_conv_layer,以下是其输入。

  • input:一个4维重塑的输入占位符变量,即x_image。
  • num_input_channels:彩色通道的数量,即num_channels。
  • filter_size:过滤器层的高度和宽度,即filter_size1。
  • num_filters:过滤层的深度,即num_filters1。
  • use_pooling:设置为正确(TRUE)的二进制标志。

3.9.2 怎么做

1.使用前面的输入参数运行create_conv_layer函数:

#  卷积层1conv1 

2.提取第一个卷积层的层(layers):

layer_conv1 

3.提取第一个卷积层的最终权重(weights):

weights_conv1 

4.生成第一个卷积层绘图:

drawImage_conv(sample(1:50000, size=1), images.bw = conv1_images,images.lab=images.lab.train)

5.生成第一个卷积层的权重图:

drawImage_conv_weights(weights_conv1)

3.9.3 工作原理

在3.9.2小节的步骤1和步骤2中,我们创建了第一个4维的卷积层:第一维度表示任意数量的输入图像;第二维度和第三维度表示每个卷积图像的高度(16个像素)和宽度(16个像素);第4个维度表示生成的通道(64), 每个卷积过滤器一个。在3.9.2小节的步骤3和步骤5中,我们提取卷积层的最终权重并绘图,如图3-3所示。在3.9.2小节的步骤4中,我们绘制第一个卷积层的输出,如图3-4所示。

3d9e349f3a679f7f6eb145ad4e1929dc.png

图3-3

c03dbed6d923c366dee5e54379f28a05.png

图3-4

3.10 创建第二个卷积层

在本节中,我们来创建第二个卷积层。

3.10.1 做好准备

在“使用函数创建一个新的卷积层”一节(见3.5节)中定义了函数create_conv_layer,以下是其输入。

  • input:第一个卷积层的四维输出,即layer_conv1。
  • num_input_channels:第一个卷积层中过滤器的数目(或深度),即num_filters1。
  • filter_size:过滤器层的高度和宽度,即filter_size2。
  • num_filters:过滤器层的深度,即num_filters2。
  • use_pooling:设置为正确(TRUE)的二进制标志。

3.10.2 怎么做

1.使用前面的输入参数运行create_conv_layer函数:

#  卷积层2conv2 

2.提取第二个卷积层的层(layers):

layer_conv2 

3.提取第二个卷积层的最终权重(weights):

weights_conv2 

4.生成第二个卷积层绘图:

drawImage_conv(sample(1:50000, size=1), images.bw = conv2_images,images.lab=images.lab.train)

5.生成第二个卷积层的权重图:

drawImage_conv_weights(weights_conv2)

3.10.3 工作原理

在3.10.2小节的步骤1和步骤2中,我们创建了第二个4维卷积层:第一维度表示任意数量的输入图像;第二维度和第三维度表示每张卷积图像的高度(8个像素)和宽度(8个像素);第四维度表示产生的通道数量(64),每个卷积过滤器一个。

在3.10.2小节的步骤3和步骤5中,我们提取卷积层的最终权重并绘图,如图3-5所示。

40f8e251330b1374cf346407d39a115a.png

图3-5

在3.10.2小节的步骤4中,我们绘制第二个卷积层的输出,如图3-6所示。

1e8717c5e546e2026b909f5845a6eb66.png

图3-6

3.11 扁平化第二个卷积层

在本节中,我们扁平化创建的第二个卷积层。

3.11.1 做好准备

以下是在“创建第二个卷积层”一节(见3.10节)中定义的函数flatten_conv_layer的输入。

  • Layer:第二个卷积层的输出,即layer_conv2。

3.11.2 怎么做

1.使用前面的输入参数运行flatten_conv_layer函数:

flatten_lay 

2.提取扁平化层:

layer_flat 

3.提取为每张图生成的特征(输入)的数量:

num_features 

3.11.3 工作原理

在将第二卷积层的输出与完全连接网络连接之前,在3.11.2小节的步骤1中,我们将4维卷积层重塑为二维张量;第一维表示任意数量的输入图像(作为行);第二维表示为每个长度为4,096的图像生成的特征的扁平化向量,即8×8×64(作为列)。3.11.2小节的步骤2和步骤3验证了重塑层的维度和输入特征。

3.12 创建第一个完全连接的层

在本节中,我们来创建第一个完全连接的层。

3.12.1 做好准备

以下是在“使用函数扁平化密集连接层”一节(见3.7节)中定义的函数create_fc_layer的输入。

  • input:扁平的卷积层,即layer_flat。
  • num_inputs:扁平后创建的特征的数量,即num_features。
  • num_outputs:完全连接的神经元输出的数量,即fc_size。
  • use_relu:设置为正确(TRUE)的二进制标志,以便在张量中引入非线性。

3.12.2 怎么做

使用前面的输入参数运行create_fc_layer函数:

layer_fc1 = create_fc_layer(input=layer_flat,num_inputs=num_features,num_outputs=fc_size,use_relu=TRUE)

3.12.3 工作原理

此处,我们创建一个返回二维张量的完全连接层:第一维表示任意数量的图像(输入);第二维表示输出神经元的数量(此处为1,024)。

3.13 将dropout应用于第一个完全连接的层

在本节中,我们将dropout应用到完全连接层的输出,以降低过度拟合的可能性。dropout步骤包括在学习过程中随机移除一些神经元。

3.13.1 做好准备

将dropout连接到层的输出。因此,建立并加载模型初始结构。例如,在dropout当前层中定义layer_fc1,在其上应用dropout。

3.13.2 怎么做

1.为dropout创建一个可以将概率作为输入的占位符:

keep_prob 

2.使用TensorFlow的dropout函数来处理神经元输出的缩放(scaling)和遮蔽(masking):

layer_fc1_drop 

3.13.3 工作原理

在3.13.2小节的步骤1和步骤2中,我们可以根据输入概率(或百分比)丢弃(或遮蔽)输出神经元。训练期间通常允许dropout,并且可以在测试期间关闭(通过将概率设定为1或NULL)。

本文截选自:《深度学习实战手册》(R语言版)第三章部分内容。

9a76a4f7f77a0d88c647031eca67e7c1.png
  • 深度学习与R语言强强联手
  • 使用TensorFlow、H2O和MXNet解决复杂的神经网络问题
  • 全彩印刷,在异步社区免费下载源代码和彩图文件

本书将深度学习和R语言两者结合起来,帮助你解决深度学习实战中所遇到的各种问题,并且教会你掌握深度学习、神经网络和机器学习的高级技巧。本书从R语言中的各种深度学习软件包和软件库入手,带领你学习复杂的深度学习算法。首先,从构建各种神经网络模型开始,而后逐步过渡到深度学习在文本挖掘和信号处理中的应用,同时还比较了CPU和GPU的性能。
阅读完本书,你将对深度学习的架构和不同的深度学习包有一个比较深入的理解,能够为你今后碰到的项目或问题找到合适的解决方案。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值