【TensorFlow-windows】学习笔记二——低级API

前言

上一篇博客初步了解了tensorflow中建立机器学习模型的方法:可以使用eager executiongraph execution两种模式,可以使用高级API estimator中已经封装好的模型,也可以自己创建estimator,更重要的是我们也可以使用低级API自行设计模型。这里重点研究研究如何使用低级API

主要内容包含:

  • 张量、变量
  • 构建计算图及其运行
  • 可视化

国际惯例,参考博客:

tensorflow官方教程

Jupyter Notebooks里的TensorFlow图可视化

张量

**概念:**张量是对矢量和矩阵向潜在的更高维度的泛化,也就是说将一维二维扩展到N维。

**特点:**数据类型和形状(维度)

创建张量

  • tf.Variable

    tf.Variable(<initial-value>, name=<optional-name>)#初始值和名称
    
  • tf.constant

    tf.constant(value,#初始值dtype=None,#数据类型shape=None,#大小name='Const',verify_shape=False
    )
    
  • tf.placeholder

    tf.placeholder(dtype,#类型shape=None,#大小name=None
    )
    
  • tf.SparseTensor

    SparseTensor(indices, values, dense_shape)
    

    indices:是一个二维整型矩阵[N,ndims][N,ndims][N,ndims],指定了稀疏张量中非零元素的索引

    values:是一维向量[N][N][N],指定了indices所指定的非零向量的值

    dense_shape:一维向量[ndims][ndims][ndims]指定了稀疏矩阵的维度

实例

#0阶变量:标量
mammal = tf.Variable("Elephant", tf.string)
ignition = tf.Variable(451, tf.int16)
floating = tf.Variable(3.14159265359, tf.float64)
its_complicated = tf.Variable(12.3 - 4.85j, tf.complex64)
#1阶变量:向量
mystr = tf.Variable(["Hello"], tf.string)
cool_numbers  = tf.Variable([3.14159, 2.71828], tf.float32)
first_primes = tf.Variable([2, 3, 5, 7, 11], tf.int32)
its_very_complicated = tf.Variable([12.3 - 4.85j, 7.5 - 6.23j], tf.complex64)
#二阶变量:矩阵
mymat = tf.Variable([[7],[11]], tf.int16)
myxor = tf.Variable([[False, True],[True, False]], tf.bool)
linear_squares = tf.Variable([[4], [9], [16], [25]], tf.int32)
squarish_squares = tf.Variable([ [4, 9], [16, 25] ], tf.int32)
rank_of_squares = tf.rank(squarish_squares)
mymatC = tf.Variable([[7],[11]], tf.int32)
#更高阶:n维矩阵
my_image = tf.zeros([10, 299, 299, 3])  # 批 x 高 x 宽 x 通道

张量切片

  • 对于0阶张量(标量),无需索引,因为它本身就是一个数字

  • 对于1阶张量(向量),通过单一索引访问

    a=tf.constant([0.1,0.5,0.12,0.6,0.7])
    sess=tf.Session()
    print(sess.run(a[2]))#0.12
    
  • 对于2阶张量(矩阵),两个索引返回标量,一个索引返回向量

    a=tf.constant([[0.5,0.6,0.3,0.7],[1,6,7,2]])
    print(sess.run(a[1,3]))#2.0
    print(sess.run(a[1]))#[1. 6. 7. 2.]
    print(sess.run(a[:,1]))#[0.6 6. ]
    print(sess.run(a[1,:]))#[1. 6. 7. 2.]
    

获取张量的秩和维度

这里秩和数学上的秩不同,这个秩指的是多少维

print(sess.run(tf.rank(a)))#2
print(a.shape)#(2,4)

张量形状的改变

在保证元素个数相同的情况下改变张量维度,其实跟reshape一样

rank_three_tensor = tf.ones([3,4,5])
matrix = tf.reshape(rank_three_tensor, [6, 10])  
matrixB = tf.reshape(matrix, [3, -1])
matrixAlt = tf.reshape(matrixB, [4, 3, -1]) 
# 一定要保证改变维度前后,矩阵的元素个数相同
#yet_another = tf.reshape(matrixAlt, [13, 2, -1])#出错
print(matrix.shape)#(6, 10)
print(matrixB.shape)#(3, 20)
print(matrixAlt.shape)#(4, 3, 5)

张量数据类型的改变

强制类型转换,主要使用tf.cast函数

float_tensor = tf.cast(tf.constant([1, 2, 3]), dtype=tf.float32)

张量取值

直接调用eval()方法,注意必须在Session启动的时候调用

a=tf.constant([1,2,3,7])
#print(a.eval())#出错
with tf.Session() as sess:print(a.eval())#[1 2 3 7]

但是有时候也无法取值,比如变量存在容器placeholder中的时候:

b=tf.placeholder(tf.float32)
with tf.Session() as sess:print(b.eval())#报错print(b.eval(feed_dict={b:3.0}))#3.0

有时候最好还是记一下报错内容,便于后期调试:

InvalidArgumentError (see above for traceback): You must feed a value for placeholder tensor 'Placeholder_2' with dtype float

张量赋值

使用上一节提到过的assign方法,但是注意要初始化,以及赋值要在session中执行。

h=tf.Variable(10,name='h')
init=tf.initialize_all_variables()
with tf.Session() as sess:sess.run(init)print(h.eval()) #10assign_op=tf.assign(h,5)sess.run(assign_op)print(h.eval()) #5

变量

在官方文档中,虽然介绍了使用tf.Variable创建变量,但是又介绍了另一种方法tf.get_variable,从名字上看像是获取变量,这就是它的优势所在,既能创建变量还能检测变量是否已经创建,如果创建过,就可以重复使用。看一下两种方法的调用方式:

#variable创建方式
tf.Variable(<initial-value>, name=<optional-name>)
#get_variable创建方式
tf.get_variable(name,shape=None,dtype=None,initializer=None,regularizer=None,trainable=True,collections=None,caching_device=None,partitioner=None,validate_shape=True,use_resource=None,custom_getter=None,constraint=None
)

很明显的区别是:tf.Variable需要指定初始值,但是名字是选填,而tf.get_variable无需指定初始值,因为它有自己的初始器initializer,但是名字是必填的,因为涉及到重用问题

##变量创建

使用get_variable定义变量时必须指定变量名称,其它副本将使用此名称访问同一变量

#创建名为`my_variable`的变量,大小为[1,2,3],值将通过`glorot_uniform_initializer`随机赋值
my_variable=tf.get_variable('my_vairable',[1,2,3])
#也可以指定类型和初始化方法
my_int_variable = tf.get_variable("my_int_variable", [1, 2, 3], dtype=tf.int32,initializer=tf.zeros_initializer)
#也可以用constant赋值,此时无需指定形状
other_variable = tf.get_variable("other_variable", dtype=tf.int32,initializer=tf.constant([23, 42]))

##变量集合

便于以一种方式访问所有变量,默认情况下,有两种集合:

  • tf.GraphKeys.GLOBAL_VARIABLES:可以在多台设备间共享变量
  • tf.GraphKeys.TRAINABLE_VARIABLES:将计算梯度的变量

如果不希望变量可训练,就放到tf.GraphKeys.LOCAL_VARIABLES中,或者指定trainable=False

my_local=tf.get_variable('my_local',shape=(),collections=[tf.GraphKeys.LOCAL_VARIABLES])
my_non_trainable=tf.get_variable('my_none_trainable',shape=(),trainable=False)

这是在创建的时候丢进去,也可以先创建一个属于自己的集合,然后挨个加:

tf.add_to_collection('my_collection_name',my_local)
tf.add_to_collection('my_collection_name',my_non_trainable)
tf.get_collection('my_collection_name')
'''
[<tf.Variable 'my_local:0' shape=() dtype=float32_ref>,<tf.Variable 'my_none_trainable:0' shape=() dtype=float32_ref>]
'''

##变量初始化

变量初始化作用在于:允许你从检查点加载模型的时候无需重新运行潜在资源消耗大的初始化器,并允许在分布式设置中共享随机初始化的变量时具有确定性。可以选择一次性初始化或者是单独初始化:

  • 如需一次性初始化所有可训练变量,调用tf.global_variables_initializer(),函数返回一个操作,负责初始化tf.GraphKeys.GLOCAL_VARIABLES集合中所有变量:

    session.run(tf.global_variables_initializer())
    
  • 如果需要自行初始化变量,可以使用:

    session.run(my_variable.initializer)
    

可以查询到哪些变量未初始化:

print(session.run(tf.report_uninitialized_variables()))

需要注意的一点是:tf.global_variables_initializer不会指定变量的初始化顺序,因此,如果变量的初始值取决于另一变量的值,那么很有可能会出现错误。如果在初始化某个变量时使用了另一个变量值,最好使用variable.initialized_value(),而非variable:

v = tf.get_variable("v", shape=(), initializer=tf.zeros_initializer())
w = tf.get_variable("w", initializer=v.initialized_value() + 1)

##变量使用

当成常规变量使用就行了,可以使用assignassign_add等方法:

import tensorflow as tf
v = tf.get_variable("v", shape=(), initializer=tf.zeros_initializer())
assignment = v.assign_add(1)
sess=tf.Session()
sess.run(tf.global_variables_initializer())
sess.run(assignment)  #1.0

在某个事件发生后,强制读取某个变量的值:

with tf.control_dependencies([assignment]):w=v.read_value()
sess.run(w)

此程序每运行一次,www的值就加1,看样子这个tf.control_dependcies是强制运行所指定语句,依据执行结果进入内部操作,这个可以看上一篇博客的Assert条件语句部分。

##共享变量

终于还是到这里了,在theano中是直接使用theano.shared创建共享变量。为了理解tensorflow中的共享变量使用方法,直接看官网的实例:

编写一个函数创建卷积/relu层:

def conv_relu(input,kernel_shape,bias_shape):#创建权重weights=tf.get_variable('weights',kernel_shape,initializer=tf.random_normal_initializer())#创建偏置biases=tf.get_variable('biases',bias_shape,initializer=tf.constant_initializer(0.0))conv=tf.nn.conv2d(input,weights,strides=[1,1,1,1],padding='SAME')return tf.nn.relu(conv+biases)

如果在多次卷积操作中,如果多次调用次函数,就会出现不清晰的操作,因为第一次执行此函数的时候已经创建了权重和偏置,那么下一次是重复利用还是创建新变量呢?这就导致程序报错

input1 = tf.random_normal([1,10,10,32])
input2 = tf.random_normal([1,20,20,32])
x = conv_relu(input1, kernel_shape=[5, 5, 32, 32], bias_shape=[32])
x = conv_relu(x, kernel_shape=[5, 5, 32, 32], bias_shape = [32])  # This fails.

错误内容:

ValueError: Variable weights already exists, disallowed. Did you mean to set reuse=True or reuse=tf.AUTO_REUSE in VarScope? Originally defined at:

如果是想创建新变量,可以为每个操作添加不同的作用域:

def my_image_filter(input_images):with tf.variable_scope('conv1'):#"conv1/weights", "conv1/biases"relu1=conv_relu(input_images,[5,5,32,32],[32])with tf.variable_scope('conv2'):#"conv2/weights", "conv2/biases"return conv_relu(relu1,[5,5,32,32],[32])

如果是共享变量,有两种方法,一种是resuse=True创建相同名称的作用域

#第一种写法
with tf.variable_scope('model'):output1=my_image_filter(input1)
with tf.variable_scope('model',reuse=True):output2=my_image_filter(input2)
#第二种写法
with tf.variable_scope('model') as scope:output1=my_image_filter(input1)
with tf.variable_scope(scope,reuse=True):output2=my_image_filter(input2)

或者直接调用scope.reuse_variables()触发重用:

with tf.variable_scope('model') as scope:output1=my_image_filter(input1)scope.reuse_variables()output2=my_image_filter(input2)

计算图的构建与运行

tf.Graph包含两类信息:

  • 图结构:包括节点和边缘,表示各个操作组合在一起
  • 图集合:在图中存储元数据集合的通用机制。与变量集合用到的方法一样tf.add_to_collection

构建图的时候:通过tf.Operation构建图节点,tf.Tensor构建图边缘

运行图的时候用tf.Session即可

x = tf.constant([[37.0, -23.0], [1.0, 4.0]])
w = tf.Variable(tf.random_uniform([2, 2]))
y = tf.matmul(x, w)
output = tf.nn.softmax(y)
init_op = w.initializerwith tf.Session() as sess:sess.run(init_op)print(sess.run(output))y_val, output_val = sess.run([y, output])

比较方便的一点是,tf.Session.run支持喂数据,在执行时使用Feed字典替换张量值

x = tf.placeholder(tf.float32, shape=[3])
y = tf.square(x)
with tf.Session() as sess:print(sess.run(y, {x: [1.0, 2.0, 3.0]}))  # => "[1.0, 4.0, 9.0]"print(sess.run(y, {x: [0.0, 0.0, 5.0]}))  # => "[0.0, 0.0, 25.0]"sess.run(y)#报错,必须喂数据sess.run(y, {x: 37.0})

其实还可以多个图进行编程,但是为了便于消化,先不做了解了,做个备注,以后戳这里学习

图的可视化

使用graphviz可视化

创建一个函数,接收的是tensorflow创建的图模型,然后分别将节点和边存储到dot中

def tf_to_dot(graph):dot=Digraph()for n in g.as_graph_def().node:dot.node(n.name,labels=n.name)for i in n.input:dot.edge(i,n.name)return dot

添加一个实例,调用上面的函数

g=tf.Graph()
with g.as_default():X=tf.placeholder(tf.float32,name='X')W1=tf.placeholder(tf.float32,name='W1')b1=tf.placeholder(tf.float32,name='b1')    a1=tf.nn.relu(tf.matmul(X,W1)+b1)W2=tf.placeholder(tf.float32,name='W2')b2=tf.placeholder(tf.float32,name='b2')a2=tf.nn.relu(tf.matmul(a1,W2)+b2)W3=tf.placeholder(tf.float32,name='W3')b3=tf.placeholder(tf.float32,name='b3')y_hat=tf.matmul(a2,W3)+b3
tf_to_dot(g)

结果:

这里写图片描述

使用tensorboard可视化

只需要将第一种方法的可视化算法换成如下即可:

import tensorflow as tf
g=tf.Graph()
with g.as_default():X=tf.placeholder(tf.float32,name='X')W1=tf.placeholder(tf.float32,name='W1')b1=tf.placeholder(tf.float32,name='b1')    a1=tf.nn.relu(tf.matmul(X,W1)+b1)W2=tf.placeholder(tf.float32,name='W2')b2=tf.placeholder(tf.float32,name='b2')a2=tf.nn.relu(tf.matmul(a1,W2)+b2)W3=tf.placeholder(tf.float32,name='W3')b3=tf.placeholder(tf.float32,name='b3')y_hat=tf.matmul(a2,W3)+b3tf.summary.FileWriter('logs',g).close()#换成这个

检查一下logs文件夹中是否有events文件,最后我们就可以去logs的上级目录打开cmd窗口输入:

tensorboard --logdir=logs

这里写图片描述

赋值网址,在浏览器中打开,便可以看到模型

这里写图片描述

有时候网络结构太大了,我们可以把每层的具体运算用scope封装起来命个名,比如第一层,第二层啥的:

import tensorflow as tf
g=tf.Graph()
with g.as_default():X=tf.placeholder(tf.float32,name='X')with tf.name_scope('Layer1'):W1=tf.placeholder(tf.float32,name='W1')b1=tf.placeholder(tf.float32,name='b1')    a1=tf.nn.relu(tf.matmul(X,W1)+b1)with tf.name_scope('Layer2'):W2=tf.placeholder(tf.float32,name='W2')b2=tf.placeholder(tf.float32,name='b2')a2=tf.nn.relu(tf.matmul(a1,W2)+b2)with tf.name_scope('Layer3'):W3=tf.placeholder(tf.float32,name='W3')b3=tf.placeholder(tf.float32,name='b3')y_hat=tf.matmul(a2,W3)+b3tf.summary.FileWriter('logs',g).close()

重复上述操作,粘贴网址到浏览器得到:

这里写图片描述

#后记

这篇博客感觉学的有点杂乱,打算大体印象还是重复学习和进一步探索了变量的操作、运算图的构建和Session运行,最有用的是学了tensorboard可视化网络结构,而且很容易,就是一句话tf.summary.FileWriter('logs',g).close()即可。

还有关于使用GPU和TPU的相关内容没看,感觉初步入门的话,先把运算搞清楚再说,内存什么的以后遇到问题再折腾,有兴趣的可以去官网看看,下面有链接,这部分内容主要包含:

  • 变量的设备放置方式:如果不小心将变量放在工作器而不是参数服务器上,可能会严重减慢训练速度,最坏的情况下,可能会让每个工作器不断复制各个变量。
  • 运算或张量的设备分配:自动分配,手动分配,多GPU

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/246609.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【TensorFlow-windows】学习笔记五——自编码器

前言 上一篇博客介绍的是构建简单的CNN去识别手写数字&#xff0c;这一篇博客折腾一下自编码&#xff0c;理论很简单&#xff0c;就是实现对输入数据的重构&#xff0c;具体理论可以看我前面的【theano-windows】学习笔记十三——去噪自编码器 国际惯例&#xff0c;参考博客&…

【TensorFlow-windows】学习笔记六——变分自编码器

#前言 对理论没兴趣的直接看代码吧&#xff0c;理论一堆&#xff0c;而且还有点复杂&#xff0c;我自己的描述也不一定准确&#xff0c;但是代码就两三句话搞定了。 国际惯例&#xff0c;参考博文 论文&#xff1a;Tutorial on Variational Autoencoders 【干货】一文读懂…

【TensorFlow-windows】学习笔记七——生成对抗网络

前言 既然学习了变分自编码(VAE)&#xff0c;那也必须来一波生成对抗网络(GAN)。 国际惯例&#xff0c;参考网址&#xff1a; 论文: Generative Adversarial Nets PPT:Generative Adversarial Networks (GANs) Generative Adversarial Nets in TensorFlow GAN原理学习笔记…

Openpose——windows编译(炒鸡简单)

前言 最近准备看看rtpose的代码&#xff0c;发现已经由openpose这个项目维护着了&#xff0c;由于经常在windows下调试代码&#xff0c;所以尝试了一下如何在windows下编译openpose源码&#xff0c;整体来说非常简单的。 国际惯例&#xff0c;参考博客&#xff1a; [OpenPos…

强化学习——Qlearning

前言 在控制决策领域里面强化学习还是占很重比例的&#xff0c;最近出了几篇角色控制的论文需要研究&#xff0c;其中部分涉及到强化学习&#xff0c;都有开源&#xff0c;有兴趣可以点开看看&#xff1a; A Deep Learning Framework For Character Motion Synthesis and Edit…

【TensorFlow-windows】keras接口学习——线性回归与简单的分类

前言 之前有写过几篇TensorFlow相关文章&#xff0c;但是用的比较底层的写法&#xff0c;比如tf.nn和tf.layers&#xff0c;也写了部分基本模型如自编码和对抗网络等&#xff0c;感觉写起来不太舒服&#xff0c;最近看官方文档发现它的教程基本都使用的keras API&#xff0c;这…

【TensorFlow-windows】keras接口——卷积手写数字识别,模型保存和调用

前言 上一节学习了以TensorFlow为底端的keras接口最简单的使用&#xff0c;这里就继续学习怎么写卷积分类模型和各种保存方法(仅保存权重、权重和网络结构同时保存) 国际惯例&#xff0c;参考博客&#xff1a; 官方教程 【注】其实不用看博客&#xff0c;直接翻到文末看我的c…

【TensorFlow-windows】keras接口——BatchNorm和ResNet

前言 之前学习利用Keras简单地堆叠卷积网络去构建分类模型的方法&#xff0c;但是对于很深的网络结构很难保证梯度在各层能够正常传播&#xff0c;经常发生梯度消失、梯度爆炸或者其它奇奇怪怪的问题。为了解决这类问题&#xff0c;大佬们想了各种办法&#xff0c;比如最原始的…

【TensorFlow-windows】keras接口——卷积核可视化

前言 在机器之心上看到了关于卷积核可视化相关理论&#xff0c;但是作者的源代码是基于fastai写的&#xff0c;而fastai的底层是pytorch&#xff0c;本来准备自己用Keras复现一遍的&#xff0c;但是尴尬地发现Keras还没玩熟练&#xff0c;随后发现了一个keras-vis包可以用于做…

【TensorFlow-windows】投影变换

前言 没什么重要的&#xff0c;就是想测试一下tensorflow的投影变换函数tf.contrib.image.transform中每个参数的含义 国际惯例&#xff0c;参考文档 官方文档 描述 调用方法与默认参数&#xff1a; tf.contrib.image.transform(images,transforms,interpolationNEAREST,…

【TensorFlow-windows】扩展层之STN

前言 读TensorFlow相关代码看到了STN的应用&#xff0c;搜索以后发现可替代池化&#xff0c;增强网络对图像变换(旋转、缩放、偏移等)的抗干扰能力&#xff0c;简单说就是提高卷积神经网络的空间不变性。 国际惯例&#xff0c;参考博客&#xff1a; 理解Spatial Transformer…

【TensorFlow-windows】MobileNet理论概览与实现

前言 轻量级神经网络中&#xff0c;比较重要的有MobileNet和ShuffleNet&#xff0c;其实还有其它的&#xff0c;比如SqueezeNet、Xception等。 本博客为MobileNet的前两个版本的理论简介与Keras中封装好的模块的对应实现方案。 国际惯例&#xff0c;参考博客&#xff1a; 纵…

【TensorFlow-windows】keras接口——ImageDataGenerator裁剪

前言 Keras中有一个图像数据处理器ImageDataGenerator&#xff0c;能够很方便地进行数据增强&#xff0c;并且从文件中批量加载图片&#xff0c;避免数据集过大时&#xff0c;一下子加载进内存会崩掉。但是从官方文档发现&#xff0c;并没有一个比较重要的图像增强方式&#x…

【TensorFlow-windows】TensorBoard可视化

前言 紧接上一篇博客&#xff0c;学习tensorboard可视化训练过程。 国际惯例&#xff0c;参考博客&#xff1a; MNIST机器学习入门 Tensorboard 详解&#xff08;上篇&#xff09; Tensorboard 可视化好帮手 2 tf-dev-summit-tensorboard-tutorial tensorflow官方mnist_…

深度学习特征归一化方法——BN、LN、IN、GN

前言 最近看到Group Normalization的论文&#xff0c;主要提到了四个特征归一化方法&#xff1a;Batch Norm、Layer Norm、Instance Norm、Group Norm。此外&#xff0c;论文还提到了Local Response Normalization(LRN)、Weight Normalization(WN)、Batch Renormalization(BR)…

【TensorFlow-windows】keras接口——利用tensorflow的方法加载数据

前言 之前使用tensorflow和keras的时候&#xff0c;都各自有一套数据读取方法&#xff0c;但是遇到一个问题就是&#xff0c;在训练的时候&#xff0c;GPU的利用率忽高忽低&#xff0c;极大可能是由于训练过程中读取每个batch数据造成的&#xff0c;所以又看了tensorflow官方的…

骨骼动画——论文与代码精读《Phase-Functioned Neural Networks for Character Control》

前言 最近一直玩CV&#xff0c;对之前学的动捕知识都忘得差不多了&#xff0c;最近要好好总结一下一直以来学习的内容&#xff0c;不能学了忘。对2017年的SIGGRAPH论文《Phase-Functioned Neural Networks for Character Control》进行一波深入剖析吧&#xff0c;结合源码。 额…

颜色协调模型Color Harmoniztion

前言 最近做换脸&#xff0c;在肤色调整的那一块&#xff0c;看到一个有意思的文章&#xff0c;复现一波玩玩。不过最后一步掉链子了&#xff0c;有兴趣的可以一起讨论把链子补上。 主要是github上大佬的那个复现代码和原文有点差异&#xff0c;而且代码复杂度过高&#xff0…

Openpose推断阶段原理

前言 之前出过一个关于openpose配置的博客&#xff0c;不过那个代码虽然写的很好&#xff0c;而且是官方的&#xff0c;但是分析起来很困难&#xff0c;然后再opencv相关博客中找到了比较清晰的实现&#xff0c;这里分析一波openpose的推断过程。 国际惯例&#xff0c;参考博…

换脸系列——眼鼻口替换

前言 想着整理一下换脸相关的技术方法&#xff0c;免得以后忘记了&#xff0c;最近脑袋越来越不好使了。应该会包含三个系列&#xff1a; 仅换眼口鼻&#xff1b;换整个面部&#xff1b;3D换脸 先看看2D换脸吧&#xff0c;网上已经有现成的教程了&#xff0c;这里拿过来整理一…