Tensorflow,CNN和MNIST数据 识别手写的数字(入门,完整代码,问题解析)

MNIST解析:

1. 导入所需模块:

#读图
from PIL import Image
#显示
import matplotlib.pyplot as plt
#TensorFlow
import tensorflow as tf
#MNIST数据
from tensorflow.examples.tutorials.mnist import input_data

2.下载并读入MNIST数据

mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

3.定义网络结构:

def conv2d(x,w):
    return tf.nn.conv2d(x,w,strides=[1,1,1,1],padding='SAME') 
#自定义池化函数 
def max_pool_2x2(x):
    return tf.nn.max_pool(x,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')
#设置占位符,尺寸为样本输入和输出的尺寸
x=tf.placeholder(tf.float32,[None,784])
y_=tf.placeholder(tf.float32,[None,10])
x_img=tf.reshape(x,[-1,28,28,1])

#设置第一个卷积层和池化层
w_conv1=tf.Variable(tf.truncated_normal([3,3,1,16],stddev=0.1))

print(w_conv1.name)
b_conv1=tf.Variable(tf.constant(0.1,shape=[16]))
h_conv1=tf.nn.relu(conv2d(x_img,w_conv1)+b_conv1)
h_pool1=max_pool_2x2(h_conv1)

#设置第二个卷积层和池化层
w_conv2=tf.Variable(tf.truncated_normal([3,3,16,25],stddev=0.1))
b_conv2=tf.Variable(tf.constant(0.1,shape=[25]))
h_conv2=tf.nn.relu(conv2d(h_pool1,w_conv2)+b_conv2)
h_pool2=max_pool_2x2(h_conv2)

#设置第一个全连接层
w_fc1=tf.Variable(tf.truncated_normal([7*7*25,516],stddev=0.1))
b_fc1=tf.Variable(tf.constant(0.1,shape=[516]))
h_pool2_flat=tf.reshape(h_pool2,[-1,7*7*25])
h_fc1=tf.nn.relu(tf.matmul(h_pool2_flat,w_fc1)+b_fc1)

#dropout(随机权重失活)
keep_prob=tf.placeholder(tf.float32)
h_fc1_drop=tf.nn.dropout(h_fc1,keep_prob)

#设置第二个全连接层
w_fc2=tf.Variable(tf.truncated_normal([516,10],stddev=0.1))
b_fc2=tf.Variable(tf.constant(0.1,shape=[10]))
y_out=tf.nn.softmax(tf.matmul(h_fc1_drop,w_fc2)+b_fc2)

#建立loss function,为交叉熵
loss=tf.reduce_mean(-tf.reduce_sum(y_*tf.log(y_out),reduction_indices=[1]))
#配置Adam优化器,学习速率为1e-4
train_step=tf.train.AdamOptimizer(1e-4).minimize(loss)

#建立正确率计算表达式
correct_prediction=tf.equal(tf.argmax(y_out,1),tf.argmax(y_,1))
accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32))

4.设置参数 isTrain ,为True表示训练,为False使用自己输入的图像测试

isTrain = True

5.开始训练

#开始数据训练
sess=tf.InteractiveSession()
saver = tf.train.Saver()  # defaults to saving all variables,只保存最后一代的模型
tf.global_variables_initializer().run()
tf.reset_default_graph()
#训练
if isTrain:
    for i in range(800): 
        batch=mnist.train.next_batch(20)
        
        if i%100==0:
            train_accuracy=accuracy.eval(feed_dict={x:batch[0],y_:batch[1],keep_prob:1})
            print ("step %d,train_accuracy= %g"%(i,train_accuracy))
        
        train_step.run(feed_dict={x:batch[0],y_:batch[1],keep_prob:0.5})    
    saver.save(sess, 'F:/MNIST/model.ckpt')    
#saver.save(sess, 'F:/MNIST/model.ckpt',global_step=i+1)  #保存模型参数,注意把这里改为自己的路径,第三个参数将训练的次数作为后缀加入到模型名字中。
#训练之后,使用测试集进行测试,输出最终结果
    print ("test_accuracy= %g"%accuracy.eval(feed_dict={x:mnist.test.images[:100],y_:mnist.test.labels[:100],keep_prob:1}))
#输入自己的数据
else:
    #saver.restore(sess, "F:/MNIST/model.ckpt")#这里使用了之前保存的模型参数
    result=imageprepare()
    ckpt = tf.train.get_checkpoint_state('F:/MNIST/')
    if ckpt and ckpt.model_checkpoint_path:
        saver.restore(sess, ckpt.model_checkpoint_path)
        print ("Model restored.")
    else:
        pass
    
    prediction=tf.argmax(y_out,1)
    predint=prediction.eval(feed_dict={x: [result],keep_prob: 1.0}, session=sess)

    print('recognize result:')
    print(predint[0])

6.使用自己的数据测试

def imageprepare():
    """
    This function returns the pixel values.
    The imput is a png file location.
    """
    file_name='F:/MNIST/11.jpg'#导入自己的图片地址
    #in terminal 'mogrify -format png *.jpg' convert jpg to png
    im = Image.open(file_name).convert('L')


    #im.save("/home/mzm/MNIST_recognize/sample.png")
    plt.imshow(im)
    plt.show()
    tv = list(im.getdata()) #get pixel values

    #normalize pixels to 0 and 1. 0 is pure white, 1 is pure black.
    tva = [ (255-x)*1.0/255.0 for x in tv] 
    #print(tva)
    return tva

将此段代码加在定义卷积和池化函数之后,将isTrain改为False,还要讲自己想测试的那张图放入imageprepare()函数中file_name的路径下,也可以自己定义。即可运行。

 

我最开始是参考链接①中的代码,发现出现了如下的几个错误:

1.内存不够:

于是我更改了网络的大小和训练时候的batchsize,如果你们电脑内存比较多就按照①中的参数来。不过看他博客下的反应貌似效果也没有特别好。

测试的结果一是和网络结构有关,也和网络参数有关,同时也和你自己的本身图片有关。注意MNIST都是黑白色构成(这里的黑色是一个0-1的浮点数,黑色越深表示数值越靠近1),如果你的测试图片和训练集相差很远,那也别求准确率很高了。

2.载入模型的时候会出现Key Variable_*** not found in checkpoint的解决思路

我的错误:

NotFoundError: Restoring from checkpoint failed. This is most likely due to a Variable name or other graph key that is missing from the checkpoint. Please ensure that you have not altered the graph expected based on the checkpoint. Original error:

Key Variable_10 not found in checkpoint
     [[{{node save_1/RestoreV2}} = RestoreV2[dtypes=[DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, ..., DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT], _device="/job:localhost/replica:0/task:0/device:CPU:0"](_arg_save_1/Const_0_0, save_1/RestoreV2/tensor_names, save_1/RestoreV2/shape_and_slices)]]

为此我查看了很多博客,在https://blog.csdn.net/qq_37353105/article/details/80467091 中提到了四点原因:

1、首先是自己定义的参数变量是否和保存的参数变量的类型是一致的

2、在文件下面是否有一个叫做chockpoint的东西存在

3、最后如果运行多次出现NotFoundError (see above for traceback): Key Variable_4 not found in checkpoint     [[Node: save_2/RestoreV2 = RestoreV2[dtypes=[DT_FLOAT, DT_FLOAT, DT_FLOAT,这种情况,请使用tf.reset_default_graph(),还有就是这句话要放在前面一点。

4、还有就是保存好了模型,如果使用的是spyder请先关闭,之后在尝试读入模型如果以上都无法解决你的问题,请删除模型之后,重新写入,再载入试试

最开始选择了方法4但是也是偶尔成功,还是不懂为什么。

最后在师兄的帮助下弄懂了原因,找到了根治顽疾的方法:

我们在定义网络结构的时候定义变量选择的是tf.Variable,“使用tf.Variable()时,如果检测到命名冲突,系统会自己处理。使用tf.get_variable()时,系统不会处理冲突,而会报错”(本句话来自https://blog.csdn.net/u012436149/article/details/53696970)。当然tf.get_variable()必须要第一个参数name,而tf.Variable()也可以不需要加参数name.这个name是tensorflow graph中的命名,如果对此有兴趣移至本人的另一篇文章TensorFlow中的name 和python代码中的变量名

我们加载模型前需定义的和模型一样的网络结构,后加载的网络结构中的tf.Variable检测到命名冲突,会给相同的变量重新命名,导致找不到加载项,所以restore失败。

举个例子:

解决方法:

① 重启IDE咯

② 在训练前重置默认模型,即加上这句话:

tf.reset_default_graph()

③ 使用get_variable代替Variable(代码中的八处都需要改,注意变量的name不能一样否则会报错)

# w_conv1=tf.Variable(tf.truncated_normal([3,3,1,16],stddev=0.1))
    w_conv1=tf.get_variable(name="v1",initializer=tf.truncated_normal([3,3,1,16],stddev=0.1))

如果你用的也是Spyder的IDE ,要在整体代码前加入tf.reset_default_graph(),否则你跑过一遍之后就会提示你的变量已经在默认的graph中出现,因此你需要在每次运行前清空默认graph的所有变量,以免get_variable报错。

④ Failed to rename: F:/MNIST/model.ckpt.index.tempstate6656860424833232902 to: F:/MNIST/model.ckpt.index : Access is denied.
; Input/output error
     [[{{node save/SaveV2}} = SaveV2[dtypes=[DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, ..., DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT], _device="/job:localhost/replica:0/task:0/device:CPU:0"](_arg_save/Const_0_0, save/SaveV2/tensor_names, save/SaveV2/shape_and_slices, b1/_35, b1/Adam/_37, b1/Adam_1/_39, b2/_41, b2/Adam/_43, b2/Adam_1/_45, b3/_47, b3/Adam/_49, b3/Adam_1/_51, b4/_53, b4/Adam/_55, b4/Adam_1/_57, beta1_power/_59, beta2_power/_61, v1/_63, v1/Adam/_65, v1/Adam_1/_67, v2/_69, v2/Adam/_71, v2/Adam_1/_73, v3/_75, v3/Adam/_77, v3/Adam_1/_79, v4/_81, v4/Adam/_83, v4/Adam_1/_85)]]

删掉上次运行生成的文件,重新运行

3.整体代码

eg1:使用get_variable

# -*- coding: utf-8 -*-
"""
Created on Wed Nov 14 14:46:13 2018

@author: zhang
"""

# -*- coding: utf-8 -*-
"""
Created on Tue Nov 13 13:43:03 2018

@author: zhang
"""
from PIL import Image
import tensorflow as tf
import matplotlib.pyplot as plt
#import cv2
from tensorflow.examples.tutorials.mnist import input_data

#读取数据
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

tf.reset_default_graph()

#构建cnn网络结构
#自定义卷积函数(后面卷积时就不用写太多)
def conv2d(x,w):
    return tf.nn.conv2d(x,w,strides=[1,1,1,1],padding='SAME') 
#自定义池化函数 
def max_pool_2x2(x):
    return tf.nn.max_pool(x,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')
def imageprepare():
    """
    This function returns the pixel values.
    The imput is a png file location.
    """
    file_name='F:/MNIST/11.jpg'#导入自己的图片地址
    #in terminal 'mogrify -format png *.jpg' convert jpg to png
    im = Image.open(file_name).convert('L')


    #im.save("/home/mzm/MNIST_recognize/sample.png")
    plt.imshow(im)
    plt.show()
    tv = list(im.getdata()) #get pixel values

    #normalize pixels to 0 and 1. 0 is pure white, 1 is pure black.
    tva = [ (255-x)*1.0/255.0 for x in tv] 
    #print(tva)
    return tva

    """
    This function returns the predicted integer.
    The imput is the pixel values from the imageprepare() function.
    """

    # Define the model (same as when creating the model file)

#设置占位符,尺寸为样本输入和输出的尺寸
x=tf.placeholder(tf.float32,[None,784])
y_=tf.placeholder(tf.float32,[None,10])
x_img=tf.reshape(x,[-1,28,28,1])


#tf.reset_default_graph()

#设置第一个卷积层和池化层


#with tf.variable_scope('model') as scope:
#w_conv1=tf.Variable(tf.truncated_normal([3,3,1,16],stddev=0.1))
w_conv1=tf.get_variable(name="v1",initializer=tf.truncated_normal([3,3,1,16],stddev=0.1))
print(w_conv1.name)
#b_conv1=tf.Variable(tf.constant(0.1,shape=[16]))
b_conv1=tf.get_variable(name="b1",initializer=tf.constant(0.1,shape=[16]))
h_conv1=tf.nn.relu(conv2d(x_img,w_conv1)+b_conv1)
h_pool1=max_pool_2x2(h_conv1)
    
#设置第二个卷积层和池化层
#w_conv2=tf.Variable(tf.truncated_normal([3,3,16,25],stddev=0.1))
#b_conv2=tf.Variable(tf.constant(0.1,shape=[25]))
w_conv2=tf.get_variable(name="v2",initializer=tf.truncated_normal([3,3,16,25],stddev=0.1))
b_conv2=tf.get_variable(name="b2",initializer=tf.constant(0.1,shape=[25]))
h_conv2=tf.nn.relu(conv2d(h_pool1,w_conv2)+b_conv2)
h_pool2=max_pool_2x2(h_conv2)
    
#设置第一个全连接层
#w_fc1=tf.Variable(tf.truncated_normal([7*7*25,516],stddev=0.1))
#b_fc1=tf.Variable(tf.constant(0.1,shape=[516]))
w_fc1=tf.get_variable(name="v3",initializer=tf.truncated_normal([7*7*25,516],stddev=0.1))
b_fc1=tf.get_variable(name="b3",initializer=tf.constant(0.1,shape=[516]))
h_pool2_flat=tf.reshape(h_pool2,[-1,7*7*25])
h_fc1=tf.nn.relu(tf.matmul(h_pool2_flat,w_fc1)+b_fc1)
    
#dropout(随机权重失活)
keep_prob=tf.placeholder(tf.float32)
h_fc1_drop=tf.nn.dropout(h_fc1,keep_prob)
    
#设置第二个全连接层
#w_fc2=tf.Variable(tf.truncated_normal([516,10],stddev=0.1))
#b_fc2=tf.Variable(tf.constant(0.1,shape=[10]))
w_fc2=tf.get_variable(name="v4",initializer=tf.truncated_normal([516,10],stddev=0.1))
b_fc2=tf.get_variable(name="b4",initializer=tf.constant(0.1,shape=[10]))
y_out=tf.nn.softmax(tf.matmul(h_fc1_drop,w_fc2)+b_fc2)

#建立loss function,为交叉熵
loss=tf.reduce_mean(-tf.reduce_sum(y_*tf.log(y_out),reduction_indices=[1]))
#配置Adam优化器,学习速率为1e-4
train_step=tf.train.AdamOptimizer(1e-4).minimize(loss)

#建立正确率计算表达式
correct_prediction=tf.equal(tf.argmax(y_out,1),tf.argmax(y_,1))
accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32))

#开始喂数据,训练

isTrain = False
#开始数据训练
sess=tf.InteractiveSession()
saver = tf.train.Saver()  # defaults to saving all variables,只保存最后一代的模型
tf.global_variables_initializer().run()

if isTrain:
    for i in range(800): 
        batch=mnist.train.next_batch(20)
        
        if i%100==0:
            train_accuracy=accuracy.eval(feed_dict={x:batch[0],y_:batch[1],keep_prob:1})
            print ("step %d,train_accuracy= %g"%(i,train_accuracy))
        
        train_step.run(feed_dict={x:batch[0],y_:batch[1],keep_prob:0.5})    
    saver.save(sess, 'F:/MNIST/model.ckpt')    
#saver.save(sess, 'F:/MNIST/model.ckpt',global_step=i+1)  #保存模型参数,注意把这里改为自己的路径,第三个参数将训练的次数作为后缀加入到模型名字中。
#训练之后,使用测试集进行测试,输出最终结果
    print ("test_accuracy= %g"%accuracy.eval(feed_dict={x:mnist.test.images[:100],y_:mnist.test.labels[:100],keep_prob:1}))
else:
    #saver.restore(sess, "F:/MNIST/model.ckpt")#这里使用了之前保存的模型参数
    result=imageprepare()
    ckpt = tf.train.get_checkpoint_state('F:/MNIST/')
    if ckpt and ckpt.model_checkpoint_path:
        saver.restore(sess, ckpt.model_checkpoint_path)
        print ("Model restored.")
    else:
        pass
    
    prediction=tf.argmax(y_out,1)
    predint=prediction.eval(feed_dict={x: [result],keep_prob: 1.0}, session=sess)

    print('recognize result:')
    print(predint[0])


eg2:使用variable:

# -*- coding: utf-8 -*-
"""
Created on Wed Nov 14 14:46:13 2018

@author: zhang
"""

# -*- coding: utf-8 -*-
"""
Created on Tue Nov 13 13:43:03 2018

@author: zhang
"""
from PIL import Image
import tensorflow as tf
import matplotlib.pyplot as plt
#import cv2
from tensorflow.examples.tutorials.mnist import input_data

#读取数据
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

tf.reset_default_graph()

#构建cnn网络结构
#自定义卷积函数(后面卷积时就不用写太多)
def conv2d(x,w):
    return tf.nn.conv2d(x,w,strides=[1,1,1,1],padding='SAME') 
#自定义池化函数 
def max_pool_2x2(x):
    return tf.nn.max_pool(x,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')
def imageprepare():
    """
    This function returns the pixel values.
    The imput is a png file location.
    """
    file_name='F:/MNIST/11.jpg'#导入自己的图片地址
    #in terminal 'mogrify -format png *.jpg' convert jpg to png
    im = Image.open(file_name).convert('L')


    #im.save("/home/mzm/MNIST_recognize/sample.png")
    plt.imshow(im)
    plt.show()
    tv = list(im.getdata()) #get pixel values

    #normalize pixels to 0 and 1. 0 is pure white, 1 is pure black.
    tva = [ (255-x)*1.0/255.0 for x in tv] 
    #print(tva)
    return tva

    """
    This function returns the predicted integer.
    The imput is the pixel values from the imageprepare() function.
    """

    # Define the model (same as when creating the model file)

#设置占位符,尺寸为样本输入和输出的尺寸
x=tf.placeholder(tf.float32,[None,784])
y_=tf.placeholder(tf.float32,[None,10])
x_img=tf.reshape(x,[-1,28,28,1])


#tf.reset_default_graph()

#设置第一个卷积层和池化层


#with tf.variable_scope('model') as scope:
w_conv1=tf.Variable(tf.truncated_normal([3,3,1,16],stddev=0.1))
#w_conv1=tf.get_variable(name="v1",initializer=tf.truncated_normal([3,3,1,16],stddev=0.1))
print(w_conv1.name)
b_conv1=tf.Variable(tf.constant(0.1,shape=[16]))
#b_conv1=tf.get_variable(name="b1",initializer=tf.constant(0.1,shape=[16]))
h_conv1=tf.nn.relu(conv2d(x_img,w_conv1)+b_conv1)
h_pool1=max_pool_2x2(h_conv1)
    
#设置第二个卷积层和池化层
w_conv2=tf.Variable(tf.truncated_normal([3,3,16,25],stddev=0.1))
b_conv2=tf.Variable(tf.constant(0.1,shape=[25]))
#w_conv2=tf.get_variable(name="v2",initializer=tf.truncated_normal([3,3,16,25],stddev=0.1))
#b_conv2=tf.get_variable(name="b2",initializer=tf.constant(0.1,shape=[25]))
h_conv2=tf.nn.relu(conv2d(h_pool1,w_conv2)+b_conv2)
h_pool2=max_pool_2x2(h_conv2)
    
#设置第一个全连接层
w_fc1=tf.Variable(tf.truncated_normal([7*7*25,516],stddev=0.1))
b_fc1=tf.Variable(tf.constant(0.1,shape=[516]))
#w_fc1=tf.get_variable(name="v3",initializer=tf.truncated_normal([7*7*25,516],stddev=0.1))
#b_fc1=tf.get_variable(name="b3",initializer=tf.constant(0.1,shape=[516]))
h_pool2_flat=tf.reshape(h_pool2,[-1,7*7*25])
h_fc1=tf.nn.relu(tf.matmul(h_pool2_flat,w_fc1)+b_fc1)
    
#dropout(随机权重失活)
keep_prob=tf.placeholder(tf.float32)
h_fc1_drop=tf.nn.dropout(h_fc1,keep_prob)
    
#设置第二个全连接层
w_fc2=tf.Variable(tf.truncated_normal([516,10],stddev=0.1))
b_fc2=tf.Variable(tf.constant(0.1,shape=[10]))
#w_fc2=tf.get_variable(name="v4",initializer=tf.truncated_normal([516,10],stddev=0.1))
#b_fc2=tf.get_variable(name="b4",initializer=tf.constant(0.1,shape=[10]))
y_out=tf.nn.softmax(tf.matmul(h_fc1_drop,w_fc2)+b_fc2)

#建立loss function,为交叉熵
loss=tf.reduce_mean(-tf.reduce_sum(y_*tf.log(y_out),reduction_indices=[1]))
#配置Adam优化器,学习速率为1e-4
train_step=tf.train.AdamOptimizer(1e-4).minimize(loss)

#建立正确率计算表达式
correct_prediction=tf.equal(tf.argmax(y_out,1),tf.argmax(y_,1))
accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32))

#开始喂数据,训练

isTrain = False
#开始数据训练
sess=tf.InteractiveSession()
saver = tf.train.Saver()  # defaults to saving all variables,只保存最后一代的模型
tf.global_variables_initializer().run()

if isTrain:
    for i in range(800): 
        batch=mnist.train.next_batch(20)
        
        if i%100==0:
            train_accuracy=accuracy.eval(feed_dict={x:batch[0],y_:batch[1],keep_prob:1})
            print ("step %d,train_accuracy= %g"%(i,train_accuracy))
        
        train_step.run(feed_dict={x:batch[0],y_:batch[1],keep_prob:0.5})    
    saver.save(sess, 'F:/MNIST/model.ckpt')    
#saver.save(sess, 'F:/MNIST/model.ckpt',global_step=i+1)  #保存模型参数,注意把这里改为自己的路径,第三个参数将训练的次数作为后缀加入到模型名字中。
#训练之后,使用测试集进行测试,输出最终结果
    print ("test_accuracy= %g"%accuracy.eval(feed_dict={x:mnist.test.images[:100],y_:mnist.test.labels[:100],keep_prob:1}))
else:
    #saver.restore(sess, "F:/MNIST/model.ckpt")#这里使用了之前保存的模型参数
    result=imageprepare()
    ckpt = tf.train.get_checkpoint_state('F:/MNIST/')
    if ckpt and ckpt.model_checkpoint_path:
        saver.restore(sess, ckpt.model_checkpoint_path)
        print ("Model restored.")
    else:
        pass
    
    prediction=tf.argmax(y_out,1)
    predint=prediction.eval(feed_dict={x: [result],keep_prob: 1.0}, session=sess)

    print('recognize result:')
    print(predint[0])



 

 

结果展示:

我最开始输入的是这个28*28的图像:

但是总是识别不对

于是我对图像进行了二值化,阈值选择的是100:

from PIL import Image

#  load a color image
im = Image.open('F:/MNIST/11.jpg' )#当前目录创建picture文件夹

#  convert to grey level image
Lim = im.convert('L' )
#Lim.save('pice.jpg' )

#  setup a converting table with constant threshold
threshold = 100
table = []
for i in range(256):
    if i < threshold:
        table.append(0)
    else:
        table.append(1)

# convert to binary image by the table
bim = Lim.point(table, '1' )

bim.save('F:/MNIST/111.jpg' )

二值化结果如下:

训练和测试结果如下:

 

结束~快乐的跑代码去吧

本文代码参考几篇文章,可以直接使用。

参考链接:

① https://blog.csdn.net/Sparta_117/article/details/66965760

② https://www.cnblogs.com/denny402/p/6940134.html

③ https://www.cnblogs.com/lizheng114/p/7439556.html

④ https://blog.csdn.net/legend_hua/article/details/78875625

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值