吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3837|回复: 6
收起左侧

[学习记录] tensorflow自主学习入门-从零开始篇(四)tensorflow手写数字识别加入tensorboard...

  [复制链接]
keepython 发表于 2019-8-21 22:31

借手写数字识别入门tensorflow已经有一段时间了,实话说经过初步入门的阵痛以后,学习tensorflow已经开始顺水推舟、有条不紊的进行开来,这期间有了一些感悟,记录下来,也希望对大家有所帮助。


本片文章依然是接着tensorflow的这一系列,记录整理学习轨迹。
不过,不同于上面的三篇文章,本文的内容比较的少,只是记录了使用tensorboard来实现可视化的过程。具体原因是,一方面,我认为对于tensorflow的学习应该建立在知道与部分理解的基础上来进行的,相信到目前为止,依然会对已经实现的功能有所困惑,即使没有困惑,已经实现的代码肯定有可以继续挖掘,自主学习的点,因此这一阶段放慢学习的进度是必须的。另一方面,这一段时间都是针对学习本身的记录,缺少对学习路径以及心路历程的记录,因此借由本文技术篇幅较少,记录分享出来,供各位参考。


本文我会先介绍tensorboard 及其具体使用方法(在手写数字识别上的实施),不过本次tensorboard的使用分为两个部分第二部分是第一部分的升级版本,可能有些难度,但是很酷,大家可以根据情况学习,暂时无法理解的部分可以先知道,然后暂时放一放,然后是看完本文关于 tensorboard 的信息,你应该做到的点。最后,就是我所说的分享。
希望大家有所参考,各取所需:

  1. tensorboard代码实现及详解
  2. 你应该知道、理解及做到的点
  3. 学习路径以及心路历程的记录

ps:不同于以忘的代码,这次需要注意的是还有很多的附加文件以及需要自己创建的文件夹,所以我会将本文的源码以及配置文件分享在文章的底部,想要在自己的机器上跑起来的朋友一定需要下载,同时代码中DIR定义为项目的绝对路径,大家需要视自己情况修改。


一 、tensorboard代码实现及详解

上代码:


加载MNIST数据,导入所需模块

首先依旧是载入数据集,以及import 必须的模块。
这一次你会发现多出了os模块以及:from tensorflow.contrib.tensorboard.plugins import projector这一句。导入os模块是为了关掉tensorflow在运行过程中的warning具体解释看这里,而第二个是为了实现tensorboard中的个人觉得比较酷的功能,当然暂时可以不理解,知道即可。

# coding: utf-8
import os
from tensorflow.contrib.tensorboard.plugins import projector
from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf
import time#用于获取迭代运行时间,比较不同方案
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
#加载数据
mnist  = input_data.read_data_sets("MNIST_data",one_hot=True)

注意:接下来的代码与以前的代码可能看起来有些不同(其实你仔细看实际上没有变化)


定义部分常量

这一部分的具体解释都在代码注释中,其中注意点是:

  1. DIR的写法,这里有一个小坑,没有发现可以留言询问
  2. embedding的意义
  3. 这里将
#运行次数
max_staps= 50001
image_num = 3000#最高为10000测试数据集总共有这么多的图片
#定义回归模型
keep_prob=tf.placeholder(tf.float32)
DIR = "./"
#定义会话
sess = tf.Session()
#载入图片测试集图片从第零张图片开始一直到3000张图片打包成一个包
embedding = tf.Variable(tf.stack(mnist.test.images[:image_num]),trainable=False,name='embedding')

封装一个记录函数

主要使用tensorflow方法tf.summary.scalar 记录一个值,并且给这个值一个名字,同时在tensorboard中画出直方图记录

这一部分知道即可


def variable_summaries(var):
    with tf.name_scope('summaries'):
        mean = tf.reduce_mean(var)#计算参数的平均值
        tf.summary.scalar('mean',mean)#平均值
        with tf.name_scope('stddev'):
            stddev = tf.sqrt(tf.reduce_mean(tf.square(var-mean)))#计算参数的标准差
        tf.summary.scalar('stddev',stddev)#标准差
        tf.summary.scalar('max',tf.reduce_mean(var))#最大值
        tf.summary.scalar('min',tf.reduce_min(var))#最小值
        tf.summary.histogram('histogram',var)#直方图

构建模型添加隐藏层并加入tensorboard可视化

这一步虽然看起来复杂,但是与上一篇文章的内容确实完全相同。
核心就是tf.name_scope()这个方法,以及在调用了placeholder和Variable方法后面加name='定义的名字'参数。

如果你能成功让本文的代码在你的机器上跑起来,通过对比 (DISTRIBUTIONS)里面的数据,你就会明白tf.name_scope的用法。

with tf.name_scope('input'):
    x = tf.placeholder(tf.float32, [None, 784],name='x_input')
    y = tf.placeholder(tf.float32, [None, 10],name='y_input')  ##输入的真是值的占位符

with tf.name_scope('input_reshape'):
    image_reshape_input = tf.reshape(x,[-1,28,28,1])#-1代表一次传进来任意值#维度是1
    tf.summary.image('input',image_reshape_input,10)#一共放十张图片

#创建一个简单的神经网络
with tf.name_scope('layer'):
    with tf.name_scope("weight_1"):
        W1 = tf.Variable(tf.truncated_normal([784, 2000], stddev=0.1),name='W_1')  # 初始化时一个非常重要的环节
        variable_summaries(W1)
    with tf.name_scope("biases_1"):
        b1 = tf.Variable(tf.zeros([2000]) + 0.1,name='b_2')
        variable_summaries(b1)
    with tf.name_scope('wx_plus_b1'):
        wx_plus_b1 = tf.matmul(x, W1) + b1
    with tf.name_scope('tanh_1'):
        L1 = tf.nn.tanh(wx_plus_b1)  # 使用双曲正切
        L1_drop = tf.nn.dropout(L1, keep_prob)  # tensorflow封装好的dropout函数,L1是我们需要控制的神经元,keep_prob是工作的神经元的百分比
    # 注意使用dropout后会使模型的训练速度下降

    with tf.name_scope("weight_2"):
        W2 = tf.Variable(tf.truncated_normal([2000, 1000], stddev=0.1),name='W_2')  # 增加隐藏层设置2000个神经元,这里是故意定义一个复杂的神经网络
        variable_summaries(W2)
    with tf.name_scope('biases_b2'):
        b2 = tf.Variable(tf.zeros([1000]) + 0.1,name='b_2')  # 期望其出项过度拟合的情况
        variable_summaries(b2)
    with tf.name_scope('wx_plus_b2'):
        wx_plus_b2 = tf.matmul(L1_drop, W2) + b2
    with tf.name_scope('tanh_2'):
        L2 = tf.nn.tanh(wx_plus_b2)
        L2_drop = tf.nn.dropout(L2, keep_prob)

    with tf.name_scope("weight_3"):
        W3 = tf.Variable(tf.truncated_normal([1000, 10], stddev=0.1),name='W3_output')
        variable_summaries(W3)
    with tf.name_scope("biases_3"):
        b3 = tf.Variable(tf.zeros([10]) + 0.1,name='b3_output')
        variable_summaries(b3)
    # 注意这里的知识点是tensorflow中矩阵相乘的规则
    with tf.name_scope('softmax'):
        with tf.name_scope('wx_plus_b3'):
            wx_plus_b3 = tf.matmul(L2_drop, W3) + b3
        with tf.name_scope('prediction'):
            prediction = tf.nn.softmax(wx_plus_b3)

构建评估模型并加入tensorboard可视化

这一部分关于tf,name_scope()内容和上面一部分是一样的。
但是,这一部分有新的tf.summary.scalar('名字',传入参数 )如果你成功打开tensorboard可视化界面,你会在(SCALARS)里面看到你定义的name以及对应的直方图。没错,tf.summary.scalar() 的作用就是在SCALARS里面加入对应的数据流并绘制其直方图。

其余部分与上一篇内容相似。
ps 注意:本篇文章这一部分的梯度下降学习率使用的是0.1,这个数据是经过多次训练学习出来的数据,有兴趣的朋友可以更换这里的优化器(optimizer)或者更改这里的学习率试试看,这也是学习的过程 。

#定义损失函数和优化器
with tf.name_scope('loss'):
    loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y,logits=prediction))
    #损失函数
    tf.summary.scalar('loss', loss)

with tf.name_scope('train'):
    train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)
with tf.name_scope('accuracy'):
    with tf.name_scope('correct_prediction'):
        correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(prediction, 1))  # 计算预测值和真实值
    with tf.name_scope('accuracy'):
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        tf.summary.scalar('accuracy', accuracy)

产生metadata文件用以实现最酷部分功能

实话讲,这一部分的代码是我拼凑起来的,但是找遍网上相关的代码以及解释,都还是差强人意,所以就斗胆献上解释,以供大家参考。

这一段代码的内容核心围绕两点:metadata.tsv以及projector.visualize_embeddings图片可视化

首先,metadata.tsv (制表符分隔值 格式文件是一种用于储存数据的文本格式文件,其数据以表格结构储存。每一行储存一条记录。 每条记录的各个字段间以制表符作为分隔。) 知道就好,它用于存储我们训练过程中生成的数据,用来供给可视化,所以围绕这一文件展开的操作有: 打开、写入以及使用tf自带的方法写入数据等等。

其次,这里的projector.visualize_embeddings 图片可视化,使用的是projector\data\mnist_10k_sprite.png 图片,当然对它进行了一定的分割,这些图片就是你最后在tensorboard的PROJECTOR中看到的动态可视化的过程。

#产生metadata文件
if tf.gfile.Exists(DIR+'projector/projector/metadata.tsv'):
    tf.gfile.DeleteRecursively(DIR+'projector/projector/metadata.tsv')
with open(DIR+'projector/projector/metadata.tsv','w') as f:#用写的方式去打开这个文件,如果没有这个文件,就会生成这样的一个文件
    labels = sess.run(tf.argmax(mnist.test.labels[:],1))#argmax用于求元素最大的标签的位置
    for i in range(image_num):
        f.write(str(labels[i])+'\n')

#合并所有的summary图像
merged = tf.summary.merge_all()

projector_writer = tf.summary.FileWriter(DIR+'projector/projector',sess.graph)
saver = tf.train.Saver()
config = projector.ProjectorConfig()
embed = config.embeddings.add()
embed.tensor_name = embedding.name
embed.metadata_path = DIR+'projector/projector/metadata.tsv'
embed.sprite.image_path = DIR+'projector/data/mnist_10k_sprite.png'
embed.sprite.single_image_dim.extend([28,28])
projector.visualize_embeddings(projector_writer,config)#图片可视化

迭代训练生成模型

这一部分需要注意有两点:

  1. run_option 的定义,有兴趣的朋友关于具体内容可以自行搜索
  2. summary ,_  = ... 在这里的应用,你应该注意道后面的merged参数,仔细将它和前面的参数进行对比,尤其是它的最初的定义,你一定会有所收获。
#Train开始训练
start = time.clock()
sess.run(init)
for i in range(max_staps):
    bath_xs, bath_ys = mnist.train.next_batch(100)
    run_options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE)
    run_metadata = tf.RunMetadata()
    #注意这里不同于上一部分的是模型需要传入的参数增加了keep_prob(需要运行的神经元的数量)
    summary ,_ = sess.run([merged,train_step], feed_dict={x:bath_xs,y:bath_ys,keep_prob:0.7},options=run_options,run_metadata=run_metadata)
    projector_writer.add_run_metadata(run_metadata, 'step%03d' % i)
    projector_writer.add_summary(summary, i)
    if i % 10 == 0:
        acc = sess.run(accuracy, feed_dict={x:mnist.test.images, y: mnist.test.labels,keep_prob:1})
        print("Iter " + str(i) + ",Testing Accuracy " + str(acc))

end = time.clock()
saver.save(sess,DIR+'projector/projector/a_model.ckpt',global_step=max_staps)
projector_writer.close()
sess.close()
print('Running time: %s Seconds'%int(end-start))#显示代码总的迭代使用时间
print('训练完成')

最后上总代码!:

# coding: utf-8
import os
from tensorflow.contrib.tensorboard.plugins import projector
from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf
import time#用于获取迭代运行时间,比较不同方案
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
#加载数据
mnist  = input_data.read_data_sets("MNIST_data",one_hot=True)

#运行次数
max_staps= 3001
image_num = 3000#最高为10000测试数据集总共有这么多的图片
#定义回归模型
keep_prob=tf.placeholder(tf.float32)
DIR = "C:/Users/Geek/Desktop/tensorflow/手写数字识别csdn博客"
#定义会话
sess = tf.Session()
#载入图片测试集图片从第零张图片开始一直到3000张图片打包成一个包
embedding = tf.Variable(tf.stack(mnist.test.images[:image_num]),trainable=False,name='embedding')

#参数概要
#tensorflow方法tf.summary.scalar 记录一个值,并且给这个值一个名字

def variable_summaries(var):
    with tf.name_scope('summaries'):
        mean = tf.reduce_mean(var)#计算参数的平均值
        tf.summary.scalar('mean',mean)#平均值
        with tf.name_scope('stddev'):
            stddev = tf.sqrt(tf.reduce_mean(tf.square(var-mean)))#计算参数的标准差
        tf.summary.scalar('stddev',stddev)#标准差
        tf.summary.scalar('max',tf.reduce_mean(var))#最大值
        tf.summary.scalar('min',tf.reduce_min(var))#最小值
        tf.summary.histogram('histogram',var)#直方图

with tf.name_scope('input'):
    x = tf.placeholder(tf.float32, [None, 784],name='x_input')
    y = tf.placeholder(tf.float32, [None, 10],name='y_input')  ##输入的真是值的占位符

with tf.name_scope('input_reshape'):
    image_reshape_input = tf.reshape(x,[-1,28,28,1])#-1代表一次传进来任意值#维度是1
    tf.summary.image('input',image_reshape_input,10)#一共放十张图片

#创建一个简单的神经网络
with tf.name_scope('layer'):
    with tf.name_scope("weight_1"):
        W1 = tf.Variable(tf.truncated_normal([784, 2000], stddev=0.1),name='W_1')  # 初始化时一个非常重要的环节
        variable_summaries(W1)
    with tf.name_scope("biases_1"):
        b1 = tf.Variable(tf.zeros([2000]) + 0.1,name='b_2')
        variable_summaries(b1)
    with tf.name_scope('wx_plus_b1'):
        wx_plus_b1 = tf.matmul(x, W1) + b1
    with tf.name_scope('tanh_1'):
        L1 = tf.nn.tanh(wx_plus_b1)  # 使用双曲正切
        L1_drop = tf.nn.dropout(L1, keep_prob)  # tensorflow封装好的dropout函数,L1是我们需要控制的神经元,keep_prob是工作的神经元的百分比
    # 注意使用dropout后会使模型的训练速度下降

    with tf.name_scope("weight_2"):
        W2 = tf.Variable(tf.truncated_normal([2000, 1000], stddev=0.1),name='W_2')  # 增加隐藏层设置2000个神经元,这里是故意定义一个复杂的神经网络
        variable_summaries(W2)
    with tf.name_scope('biases_b2'):
        b2 = tf.Variable(tf.zeros([1000]) + 0.1,name='b_2')  # 期望其出项过度拟合的情况
        variable_summaries(b2)
    with tf.name_scope('wx_plus_b2'):
        wx_plus_b2 = tf.matmul(L1_drop, W2) + b2
    with tf.name_scope('tanh_2'):
        L2 = tf.nn.tanh(wx_plus_b2)
        L2_drop = tf.nn.dropout(L2, keep_prob)

    with tf.name_scope("weight_3"):
        W3 = tf.Variable(tf.truncated_normal([1000, 10], stddev=0.1),name='W3_output')
        variable_summaries(W3)
    with tf.name_scope("biases_3"):
        b3 = tf.Variable(tf.zeros([10]) + 0.1,name='b3_output')
        variable_summaries(b3)
    # 注意这里的知识点是tensorflow中矩阵相乘的规则
    with tf.name_scope('softmax'):
        with tf.name_scope('wx_plus_b3'):
            wx_plus_b3 = tf.matmul(L2_drop, W3) + b3
        with tf.name_scope('prediction'):
            prediction = tf.nn.softmax(wx_plus_b3)

with tf.name_scope('loss'):
    loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y,logits=prediction))#损失函数
    tf.summary.scalar('loss', loss)
#tf.nn.softmax_cross_entropy_with_logits(logits, labels, name=None)
#除去name参数用以指定该操作的name,与方法有关的一共两个参数:
#第一个参数logits:就是神经网络最后一层的输出,未经过soft_max
#第二个参数labels:实际的标签,需要是one-hot格式

#定义损失函数和优化器
with tf.name_scope('train'):
    train_step = tf.train.GradientDescentOptimizer(0.1).minimize(loss)
#评估训练好的模型
#计算模型在测试集上的准确率
#tf.cast作用:布尔型转化为浮点数,并且取平均值,得到准确率
with tf.name_scope('accuracy'):
    with tf.name_scope('correct_prediction'):
        correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(prediction, 1))  # 计算预测值和真实值
    with tf.name_scope('accuracy'):
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        tf.summary.scalar('accuracy', accuracy)

#产生metadata文件
if tf.gfile.Exists(DIR+'projector/projector/metadata.tsv'):
    tf.gfile.DeleteRecursively(DIR+'projector/projector/metadata.tsv')
with open(DIR+'projector/projector/metadata.tsv','w') as f:#用写的方式去打开这个文件,如果没有这个文件,就会生成这样的一个文件
    labels = sess.run(tf.argmax(mnist.test.labels[:],1))#argmax用于求元素最大的标签的位置
    for i in range(image_num):
        f.write(str(labels[i])+'\n')

#合并所有的summary
merged = tf.summary.merge_all()

projector_writer = tf.summary.FileWriter(DIR+'projector/projector',sess.graph)
saver = tf.train.Saver()
config = projector.ProjectorConfig()
embed = config.embeddings.add()
embed.tensor_name = embedding.name
embed.metadata_path = DIR+'projector/projector/metadata.tsv'
embed.sprite.image_path = DIR+'projector/data/mnist_10k_sprite.png'
embed.sprite.single_image_dim.extend([28,28])
projector.visualize_embeddings(projector_writer,config)#图片可视化

init = tf.global_variables_initializer()

#Train开始训练
start = time.clock()

sess.run(init)
for i in range(max_staps):
    bath_xs, bath_ys = mnist.train.next_batch(100)
    run_options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE)
    run_metadata = tf.RunMetadata()
    #注意这里不同于上一部分的是模型需要传入的参数增加了keep_prob(需要运行的神经元的数量)
    summary ,_ = sess.run([merged,train_step], feed_dict={x:bath_xs,y:bath_ys,keep_prob:0.7},options=run_options,run_metadata=run_metadata)
    projector_writer.add_run_metadata(run_metadata, 'step%03d' % i)
    projector_writer.add_summary(summary, i)
    if i % 10 == 0:
        acc = sess.run(accuracy, feed_dict={x:mnist.test.images, y: mnist.test.labels,keep_prob:1})
        print("Iter " + str(i) + ",Testing Accuracy " + str(acc))

end = time.clock()
saver.save(sess,DIR+'projector/projector/a_model.ckpt',global_step=max_staps)
projector_writer.close()
sess.close()
print('Running time: %s Seconds'%int(end-start))
print('训练完成')

训练完成后,cmd进入第一级的projector目录输入(详见我的另一篇文章:【TensorBoard】如何启动tensorboard详解)

tensorboard --logdir=projector

二 、你应该知道、理解及做到的点

  1. 你应该知道,tensorboard的作用(为什么需要可视化、以及它的实际应用)这一点我没有在文章中说明,需要大家自行搜索。
  2. 你应该理解,tensorboard各个部分的显示内容的作用,这一点可能会有一些难度,但是个人觉得是必须的部分。
  3. 你应该做到:让本文的代码在你自己的机器上成功跑起来。成功定义:可以通过(http://localhost:6006/) 打开tensorboard面板

三 、学习路径以及心路历程的记录

现阶段作者还是新大二材料学生一枚,但是对于人工智能有很强的兴趣,加上python功底还算可以,以前做过一点scrapy爬虫和数据分析。所以,毅然的入了tensorflow的坑。

说一说目前的学习状态

暑假早晨8:00起床洗漱吃饭以后,给自己20分钟时间进入状态,开始通过百度云网盘的课程学习人工智能的算法部分,当然由于数学功底还是有所欠缺(高数8学分),需要恶补一些数学知识。这一过程有时候会开小差,看看油管视频什么的(一般看华农兄弟和美食作家王刚,小时候在城中村长大,比较喜欢他们的感觉)。

中午会去食堂吃饭(没错,我暑假留校),然后看情况会睡个午觉,主要是保持大脑清醒(学习已经非常枯燥了,怎么还能亏待身体?对吧)然后就是两个小时的打杂时间,这一部分会干一些自己其他的事,比如回回消息,刷刷新闻,偶尔会良心发泄看看英语(捂脸),目前最开心的就是看到自己的文章被点赞或者回答被采纳。

下午会开始写一些自己的东西,比如前一段时间的(算法或者tensorflow)学习笔记,或者突然自己的感想,大概会花费2-4个小时,期间累了会听听播客的(故事FM、狗熊有话说)然后吃饭、keep上跑个“法特莱跑”然后回宿舍擦擦汗平静下来,继续写东西。

写完以后基本就是休息时间了,这一段时间我会拿平板看看书,不过看情况,也可能会看美剧(毒枭1-3季),看看自己的大脑待机情况,书也不会定死,有可能是小说也有可能是工具类的书,然后累了就睡或者消遣消遣,比较随意。

一个前ACM银奖大佬给我说学习一定要形成自己的程式(个人理解就是模式与模型吧),然后一直运行,可能中间有几次中断,但是记得就好,不要过多深究,别和自己过不去,大佬今年拿到了中科大的保研,感觉大佬是一个很理性优雅的人(早就觉得程序员更像是艺术家或者是哲学家了,这是《黑客与画家》里面的观点)。

我觉得目前的学习状态就挺适合的,比较温和,当然开学后可能需要有所改进,也希望本文对你有所帮助,谢谢大家。


项目源码以及配套环境
链接: https://pan.baidu.com/s/10nw_hIFFIsdNENTq9RPh5g 提取码: 2333
ps:相关文章


最后,一如既往的:
我希望大家有问题可以私信问我,我会很愿意帮你解答,毕竟,这就是在以一种很有效率的方式帮助我的学习,加深我对它的理解。

希望本文对你有所帮助,接下来我会更新进一步优化加入卷积神经网络将识别准确率提升到99%以上 以及 关于若干tensorflow基础操作的文章。

免费评分

参与人数 3吾爱币 +7 热心值 +2 收起 理由
苏紫方璇 + 5 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
_小白 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
KARMA07007 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

xymoke 发表于 2019-8-21 22:41
授人以渔的都得赞
gada888 发表于 2019-8-21 23:14
千言万语,突然不知从哪开口。但AI的趋势是去大项目化,各方大公司都加大押注在单片机AI模块上。便携式AI项目会飞起。这样看来,你的单纯从软件角度考虑AI就有点偏。
pob777 发表于 2019-8-25 16:14
能解释下a = tf.ones([1,5,5,3])里面的1,5,5,3的含义么??
chunhwa 发表于 2019-11-3 16:15
最近一直在找这方面的资料。非常谢谢!
tytyol 发表于 2020-11-30 20:26
楼主,源码的链接还有吗
buge163 发表于 2020-12-8 11:49
感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止回复与主题无关非技术内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-25 07:16

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表