timertask run函数未执行_图执行模式下的 TensorFlow 2
文 /??李錫涵,Google Developers Expert
本文節(jié)選自《簡(jiǎn)單粗暴 TensorFlow 2.0》
盡管 TensorFlow 2 建議以即時(shí)執(zhí)行模式(Eager Execution)作為主要執(zhí)行模式,然而,圖執(zhí)行模式(Graph Execution)作為 TensorFlow 2 之前的主要執(zhí)行模式,依舊對(duì)于我們理解 TensorFlow 具有重要意義。尤其是當(dāng)我們需要使用 tf.function 時(shí),對(duì)圖執(zhí)行模式的理解更是不可或缺。
圖執(zhí)行模式在 TensorFlow 1.X 和 2.X 版本中的 API 不同:- 在 TensorFlow 1.X 中,圖執(zhí)行模式主要通過(guò) “直接構(gòu)建計(jì)算圖 +?tf.Session” 進(jìn)行操作;
在 TensorFlow 2 中,圖執(zhí)行模式主要通過(guò)?tf.function?進(jìn)行操作。
提示
TensorFlow 2 依然支持 TensorFlow 1.X 的 API。為了在 TensorFlow 2 中使用 TensorFlow 1.X 的 API ,我們可以使用?import tensorflow.compat.v1 as tf?導(dǎo)入 TensorFlow,并通過(guò)?tf.disable_eager_execution()?禁用默認(rèn)的即時(shí)執(zhí)行模式。
TensorFlow 1+1
TensorFlow 的圖執(zhí)行模式是一個(gè)符號(hào)式的(基于計(jì)算圖的)計(jì)算框架。簡(jiǎn)而言之,如果你需要進(jìn)行一系列計(jì)算,則需要依次進(jìn)行如下兩步:- 建立一個(gè) “計(jì)算圖”,這個(gè)圖描述了如何將輸入數(shù)據(jù)通過(guò)一系列計(jì)算而得到輸出;
建立一個(gè)會(huì)話,并在會(huì)話中與計(jì)算圖進(jìn)行交互,即向計(jì)算圖傳入計(jì)算所需的數(shù)據(jù),并從計(jì)算圖中獲取結(jié)果。
這里以計(jì)算 1+1 作為 Hello World 的示例。以下代碼通過(guò) TensorFlow 1.X 的圖執(zhí)行模式 API 計(jì)算 1+1:
import tensorflow.compat.v1 as tftf.disable_eager_execution()
# 以下三行定義了一個(gè)簡(jiǎn)單的“計(jì)算圖”
a = tf.constant(1) # 定義一個(gè)常量張量(Tensor)
b = tf.constant(1)
c = a + b # 等價(jià)于 c = tf.add(a, b),c是張量a和張量b通過(guò) tf.add 這一操作(Operation)所形成的新張量
# 到此為止,計(jì)算圖定義完畢,然而程序還沒(méi)有進(jìn)行任何實(shí)質(zhì)計(jì)算。
# 如果此時(shí)直接輸出張量 c 的值,是無(wú)法獲得 c = 2 的結(jié)果的
sess = tf.Session() # 實(shí)例化一個(gè)會(huì)話(Session)
c_ = sess.run(c) # 通過(guò)會(huì)話的 run() 方法對(duì)計(jì)算圖里的節(jié)點(diǎn)(張量)進(jìn)行實(shí)際的計(jì)算
print(c_)
輸出:2
而在 TensorFlow 2 中,我們將計(jì)算圖的建立步驟封裝在一個(gè)函數(shù)中,并使用?@tf.function修飾符對(duì)函數(shù)進(jìn)行修飾。當(dāng)需要運(yùn)行此計(jì)算圖時(shí),只需調(diào)用修飾后的函數(shù)即可。由此,我們可以將以上代碼改寫如下:import tensorflow as tf# 以下被 @tf.function 修飾的函數(shù)定義了一個(gè)計(jì)算圖@tf.functiondef graph():
a = tf.constant(1)
b = tf.constant(1)
c = a + breturn c# 到此為止,計(jì)算圖定義完畢。由于 graph() 是一個(gè)函數(shù),在其被調(diào)用之前,程序是不會(huì)進(jìn)行任何實(shí)質(zhì)計(jì)算的。# 只有調(diào)用函數(shù),才能通過(guò)函數(shù)返回值,獲得 c = 2 的結(jié)果
c_ = graph()
print(c_.numpy())
小結(jié)
在 TensorFlow 1.X 的 API 中,我們直接在主程序中建立計(jì)算圖。而在 TensorFlow 2 中,計(jì)算圖的建立需要被封裝在一個(gè)被 @tf.function 修飾的函數(shù)中;
在 TensorFlow 1.X 的 API 中,我們通過(guò)實(shí)例化一個(gè) tf.Session ,并使用其 run 方法執(zhí)行計(jì)算圖的實(shí)際運(yùn)算。而在 TensorFlow 2 中,我們通過(guò)直接調(diào)用被 @tf.function 修飾的函數(shù)來(lái)執(zhí)行實(shí)際運(yùn)算。
上面這個(gè)程序只能計(jì)算 1+1,以下代碼通過(guò) TensorFlow 1.X 的圖執(zhí)行模式 API 中的 tf.placeholder() (占位符張量)和 sess.run() 的 feed_dict 參數(shù),展示了如何使用 TensorFlow 計(jì)算任意兩個(gè)數(shù)的和:
import tensorflow.compat.v1 as tftf.disable_eager_execution()
a = tf.placeholder(dtype=tf.int32) # 定義一個(gè)占位符Tensor
b = tf.placeholder(dtype=tf.int32)
c = a + b
a_ = int(input("a = ")) # 從終端讀入一個(gè)整數(shù)并放入變量a_
b_ = int(input("b = "))
sess = tf.Session()
c_ = sess.run(c, feed_dict={a: a_, b: b_}) # feed_dict參數(shù)傳入為了計(jì)算c所需要的張量的值
print("a + b = %d" % c_)
運(yùn)行程序:
>>> a = 2>>> b = 3
a + b = 5
而在 TensorFlow 2 中,我們可以通過(guò)為函數(shù)指定參數(shù)來(lái)實(shí)現(xiàn)與占位符張量相同的功能。為了在計(jì)算圖運(yùn)行時(shí)送入占位符數(shù)據(jù),只需在調(diào)用被修飾后的函數(shù)時(shí),將數(shù)據(jù)作為參數(shù)傳入即可。由此,我們可以將以上代碼改寫如下:
import tensorflow as tf@tf.function
def graph(a, b):
c = a + b
return c
a_ = int(input("a = "))
b_ = int(input("b = "))
c_ = graph(a_, b_)
print("a + b = %d" % c_)
小結(jié)在 TensorFlow 1.X 的 API 中,我們使用 tf.placeholder() 在計(jì)算圖中聲明占位符張量,并通過(guò) sess.run() 的 feed_dict 參數(shù)向計(jì)算圖中的占位符傳入實(shí)際數(shù)據(jù)。而在 TensorFlow 2 中,我們使用 tf.function 的函數(shù)參數(shù)作為占位符張量,通過(guò)向被 @tf.function 修飾的函數(shù)傳遞參數(shù),來(lái)為計(jì)算圖中的占位符張量提供實(shí)際數(shù)據(jù)。
計(jì)算圖中的變量?變量的聲明?
變量(Variable)是一種特殊類型的張量,使用 tf.get_variable()建立,與編程語(yǔ)言中的變量很相似。使用變量前需要先初始化,變量?jī)?nèi)存儲(chǔ)的值可以在計(jì)算圖的計(jì)算過(guò)程中被修改。以下示例代碼展示了如何建立一個(gè)變量,將其值初始化為 0,并逐次累加 1。import tensorflow.compat.v1 as tftf.disable_eager_execution()
a = tf.get_variable(name='a', shape=[])
initializer = tf.assign(a, 0.0) # tf.assign(x, y)返回一個(gè)“將張量y的值賦給變量x”的操作
plus_one_op = tf.assign(a, a + 1.0)
sess = tf.Session()
sess.run(initializer)
for i in range(5):
sess.run(plus_one_op) # 對(duì)變量a執(zhí)行加一操作
print(sess.run(a)) # 輸出此時(shí)變量a在當(dāng)前會(huì)話的計(jì)算圖中的值
輸出:1.0
2.0
3.0
4.0
5.0
提示為了初始化變量,也可以在聲明變量時(shí)指定初始化器(initializer),并通過(guò) tf.global_variables_initializer() 一次性初始化所有變量,在實(shí)際工程中更常用:
import tensorflow.compat.v1 as tftf.disable_eager_execution()
a = tf.get_variable(name='a', shape=[],
initializer=tf.zeros_initializer) # 指定初始化器為全0初始化
plus_one_op = tf.assign(a, a + 1.0)
sess = tf.Session()
sess.run(tf.global_variables_initializer()) # 初始化所有變量
for i in range(5):
sess.run(plus_one_op)
print(sess.run(a)在 TensorFlow 2 中,我們通過(guò)實(shí)例化tf.Variable類來(lái)聲明變量。由此,我們可以將以上代碼改寫如下:import tensorflow as tf
a = tf.Variable(0.0)
@tf.function
def plus_one_op():
a.assign(a + 1.0)
return a
for i in range(5):
plus_one_op()
print(a.numpy())
小結(jié)
在 TensorFlow 1.X 的 API 中,我們使用 tf.get_variable() 在計(jì)算圖中聲明變量節(jié)點(diǎn)。而在 TensorFlow 2 中,我們直接通過(guò) tf.Variable 實(shí)例化變量對(duì)象,并在計(jì)算圖中使用這一變量對(duì)象。
import numpy as np
tf.disable_eager_execution()
def dense(inputs, num_units):
weight = tf.get_variable(name='weight', shape=[inputs.shape[1], num_units])
bias = tf.get_variable(name='bias', shape=[num_units])
return tf.nn.relu(tf.matmul(inputs, weight) + bias)
def model(inputs):
with tf.variable_scope('dense1'): # 限定變量的作用域?yàn)?dense1
x = dense(inputs, 10) # 聲明了 dense1/weight 和 dense1/bias 兩個(gè)變量
with tf.variable_scope('dense2'): # 限定變量的作用域?yàn)?dense2
x = dense(x, 10) # 聲明了 dense2/weight 和 dense2/bias 兩個(gè)變量
with tf.variable_scope('dense2', reuse=True): # 第三層復(fù)用第二層的變量
x = dense(x, 10)
return x
inputs = tf.placeholder(shape=[10, 32], dtype=tf.float32)
outputs = model(inputs)
print(tf.global_variables()) # 輸出當(dāng)前計(jì)算圖中的所有變量節(jié)點(diǎn)
sess = tf.Session()
sess.run(tf.global_variables_initializer())
outputs_ = sess.run(outputs, feed_dict={inputs: np.random.rand(10, 32)})
print(outputs_)
在上例中,計(jì)算圖的所有變量節(jié)點(diǎn)為:['dense1/weight:0' shape=(32, 10) dtype=float32>,'dense1/bias:0' shape=(10,) dtype=float32>,'dense2/weight:0' shape=(10, 10) dtype=float32>,'dense2/bias:0' shape=(10,) dtype=float32>]
可見,tf.variable_scope() 為在其上下文中的,以 tf.get_variable 建立的變量的名稱添加了 “前綴” 或 “作用域”,使得變量在計(jì)算圖中的層次結(jié)構(gòu)更為清晰,不同 “作用域” 下的同名變量各司其職,不會(huì)沖突。同時(shí),雖然我們?cè)谏侠姓{(diào)用了 3 次 dense 函數(shù),即調(diào)用了 6 次 tf.get_variable 函數(shù),但實(shí)際建立的變量節(jié)點(diǎn)只有 4 個(gè)。這即是 tf.variable_scope() 的 reuse 參數(shù)所起到的作用。當(dāng) reuse=True 時(shí), tf.get_variable 遇到重名變量時(shí)將會(huì)自動(dòng)獲取先前建立的同名變量,而不會(huì)新建變量,從而達(dá)到了變量重用的目的。
而在 TensorFlow 2 的圖執(zhí)行模式 API 中,不再鼓勵(lì)使用 tf.variable_scope() ,而應(yīng)當(dāng)使用 tf.keras.layers.Layer 和 tf.keras.Model 來(lái)封裝代碼和指定作用域,具體可參考 本手冊(cè)第三章。上面的例子與下面基于 tf.keras 和 tf.function 的代碼等價(jià)。import tensorflow as tfimport numpy as np
class Dense(tf.keras.layers.Layer):
def __init__(self, num_units, **kwargs):
super().__init__(**kwargs)
self.num_units = num_units
def build(self, input_shape):
self.weight = self.add_variable(name='weight', shape=[input_shape[-1], self.num_units])
self.bias = self.add_variable(name='bias', shape=[self.num_units])
def call(self, inputs):
y_pred = tf.matmul(inputs, self.weight) + self.bias
return y_pred
class Model(tf.keras.Model):
def __init__(self):
super().__init__()
self.dense1 = Dense(num_units=10, name='dense1')
self.dense2 = Dense(num_units=10, name='dense2')
@tf.function
def call(self, inputs):
x = self.dense1(inputs)
x = self.dense2(inputs)
x = self.dense2(inputs)
return x
model = Model()
print(model(np.random.rand(10, 32)))
我們可以注意到,在 TensorFlow 2 中,變量的作用域以及復(fù)用變量的問(wèn)題自然地淡化了。基于 Python 類的模型建立方式自然地為變量指定了作用域,而變量的重用也可以通過(guò)簡(jiǎn)單地多次調(diào)用同一個(gè)層來(lái)實(shí)現(xiàn)。
為了詳細(xì)了解上面的代碼對(duì)變量作用域的處理方式,我們使用 get_concrete_function導(dǎo)出計(jì)算圖,并輸出計(jì)算圖中的所有變量節(jié)點(diǎn):graph = model.call.get_concrete_function(np.random.rand(10, 32))print(graph.variables)
輸出如下:('dense1/weight:0' shape=(32, 10) dtype=float32, numpy=...>,'dense1/bias:0' shape=(10,) dtype=float32, numpy=...>,'dense2/weight:0' shape=(32, 10) dtype=float32, numpy=...>,'dense2/bias:0' shape=(10,) dtype=float32, numpy=...)可見,TensorFlow 2 的圖執(zhí)行模式在變量的作用域上與 TensorFlow 1.X 實(shí)際保持了一致。我們通過(guò)name參數(shù)為每個(gè)層指定的名稱將成為層內(nèi)變量的作用域。
小結(jié)
在 TensorFlow 1.X 的 API 中,使用 tf.variable_scope() 及 reuse 參數(shù)來(lái)實(shí)現(xiàn)變量作用域和復(fù)用變量的功能。在 TensorFlow 2 中,使用 tf.keras.layers.Layer 和 tf.keras.Model 來(lái)封裝代碼和指定作用域,從而使變量的作用域以及復(fù)用變量的問(wèn)題自然淡化。兩者的實(shí)質(zhì)是一樣的。
自動(dòng)求導(dǎo)機(jī)制與優(yōu)化器?
在本節(jié)中,我們對(duì) TensorFlow 1.X 和 TensorFlow 2 在圖執(zhí)行模式下的自動(dòng)求導(dǎo)機(jī)制進(jìn)行較深入的比較說(shuō)明。
自動(dòng)求導(dǎo)機(jī)制?我們首先回顧 TensorFlow 1.X 中的自動(dòng)求導(dǎo)機(jī)制。在 TensorFlow 1.X 的圖執(zhí)行模式 API 中,可以使用 tf.gradients(y, x) 計(jì)算計(jì)算圖中的張量節(jié)點(diǎn) y 相對(duì)于變量 x 的導(dǎo)數(shù)。以下示例展示了在 TensorFlow 1.X 的圖執(zhí)行模式 API 中計(jì)算在時(shí)的導(dǎo)數(shù)。x = tf.get_variable('x', dtype=tf.float32, shape=[], initializer=tf.constant_initializer(3.))y = tf.square(x) # y = x ^ 2
y_grad = tf.gradients(y, x)
以上代碼中,計(jì)算圖中的節(jié)點(diǎn) y_grad 即為 y 相對(duì)于 x 的導(dǎo)數(shù)。
而在 TensorFlow 2 的圖執(zhí)行模式 API 中,我們使用 tf.GradientTape 這一上下文管理器封裝需要求導(dǎo)的計(jì)算步驟,并使用其 gradient 方法求導(dǎo),代碼示例如下:
x = tf.Variable(3.)@tf.function
def grad():
with tf.GradientTape() as tape:
y = tf.square(x)
y_grad = tape.gradient(y, x)
return y_grad
小結(jié)
在 TensorFlow 1.X 中,我們使用 tf.gradients() 求導(dǎo)。而在 TensorFlow 2 中,我們使用使用 tf.GradientTape 這一上下文管理器封裝需要求導(dǎo)的計(jì)算步驟,并使用其 gradient 方法求導(dǎo)。
由于機(jī)器學(xué)習(xí)中的求導(dǎo)往往伴隨著優(yōu)化,所以 TensorFlow 中更常用的是優(yōu)化器(Optimizer)。在 TensorFlow 1.X 的圖執(zhí)行模式 API 中,我們往往使用tf.train中的各種優(yōu)化器,將求導(dǎo)和調(diào)整變量值的步驟合二為一。例如,以下代碼片段在計(jì)算圖構(gòu)建過(guò)程中,使用 tf.train.GradientDescentOptimizer這一梯度下降優(yōu)化器優(yōu)化損失函數(shù) loss :
y_pred = model(data_placeholder) # 模型構(gòu)建loss = ... # 計(jì)算模型的損失函數(shù) loss
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
train_one_step = optimizer.minimize(loss)
# 上面一步也可拆分為
# grad = optimizer.compute_gradients(loss)
# train_one_step = optimizer.apply_gradients(grad)
以上代碼中, train_one_step 即為一個(gè)將求導(dǎo)和變量值更新合二為一的計(jì)算圖節(jié)點(diǎn)(操作),也就是訓(xùn)練過(guò)程中的 “一步”。特別需要注意的是,對(duì)于優(yōu)化器的 minimize 方法而言,只需要指定待優(yōu)化的損失函數(shù)張量節(jié)點(diǎn) loss 即可,求導(dǎo)的變量可以自動(dòng)從計(jì)算圖中獲得(即 tf.trainable_variables )。在計(jì)算圖構(gòu)建完成后,只需啟動(dòng)會(huì)話,使用 sess.run 方法運(yùn)行 train_one_step 這一計(jì)算圖節(jié)點(diǎn),并通過(guò) feed_dict 參數(shù)送入訓(xùn)練數(shù)據(jù),即可完成一步訓(xùn)練。代碼片段如下:
for data in dataset:data_dict = ... # 將訓(xùn)練所需數(shù)據(jù)放入字典 data 內(nèi)
sess.run(train_one_step, feed_dict=data_dict)
而在 TensorFlow 2 的 API 中,無(wú)論是圖執(zhí)行模式還是即時(shí)執(zhí)行模式,均先使用 tf.GradientTape 進(jìn)行求導(dǎo)操作,然后再使用優(yōu)化器的 apply_gradients 方法應(yīng)用已求得的導(dǎo)數(shù),進(jìn)行變量值的更新。也就是說(shuō),和 TensorFlow 1.X 中優(yōu)化器的 compute_gradients + apply_gradients 十分類似。同時(shí),在 TensorFlow 2 中,無(wú)論是求導(dǎo)還是使用導(dǎo)數(shù)更新變量值,都需要顯式地指定變量。計(jì)算圖的構(gòu)建代碼結(jié)構(gòu)如下:
optimizer = tf.keras.optimizer.SGD(learning_rate=...)@tf.function
def train_one_step(data):
with tf.GradientTape() as tape:
y_pred = model(data) # 模型構(gòu)建
loss = ... # 計(jì)算模型的損失函數(shù) loss
grad = tape.gradient(loss, model.variables)
optimizer.apply_gradients(grads_and_vars=zip(grads, model.variables))在計(jì)算圖構(gòu)建完成后,我們直接調(diào)用 train_one_step函數(shù)并送入訓(xùn)練數(shù)據(jù)即可:for data in dataset:
train_one_step(data)
小結(jié)
在 TensorFlow 1.X 中,我們多使用優(yōu)化器的 minimize 方法,將求導(dǎo)和變量值更新合二為一。而在 TensorFlow 2 中,我們需要先使用 tf.GradientTape 進(jìn)行求導(dǎo)操作,然后再使用優(yōu)化器的 apply_gradients 方法應(yīng)用已求得的導(dǎo)數(shù),進(jìn)行變量值的更新。而且在這兩步中,都需要顯式指定待求導(dǎo)和待更新的變量。
在本節(jié),為了幫助讀者更深刻地理解 TensorFlow 的自動(dòng)求導(dǎo)機(jī)制,我們以前節(jié)的 “計(jì)算??在?時(shí)的導(dǎo)數(shù)” 為例,展示 TensorFlow 1.X 和 TensorFlow 2 在圖執(zhí)行模式下,為這一求導(dǎo)過(guò)程所建立的計(jì)算圖,并進(jìn)行詳細(xì)講解。
在 TensorFlow 1.X 的圖執(zhí)行模式 API 中,將生成的計(jì)算圖使用 TensorBoard 進(jìn)行展示:
在計(jì)算圖中,灰色的塊為節(jié)點(diǎn)的命名空間(Namespace,后文簡(jiǎn)稱 “塊”),橢圓形代表操作節(jié)點(diǎn)(OpNode),圓形代表常量,灰色的箭頭代表數(shù)據(jù)流。為了弄清計(jì)算圖節(jié)點(diǎn) x 、 y 和 y_grad 與計(jì)算圖中節(jié)點(diǎn)的對(duì)應(yīng)關(guān)系,我們將這些變量節(jié)點(diǎn)輸出,可見:- x :?
- y : Tensor("Square:0", shape=(), dtype=float32)
y_grad : []
在 TensorBoard 中,我們也可以通過(guò)點(diǎn)擊節(jié)點(diǎn)獲得節(jié)點(diǎn)名稱。通過(guò)比較我們可以得知,變量 x 對(duì)應(yīng)計(jì)算圖最下方的 x,節(jié)點(diǎn) y 對(duì)應(yīng)計(jì)算圖 “Square” 塊的 “ (Square) ”,節(jié)點(diǎn) y_grad 對(duì)應(yīng)計(jì)算圖上方 “Square_grad” 的 Mul_1 節(jié)點(diǎn)。同時(shí)我們還可以通過(guò)點(diǎn)擊節(jié)點(diǎn)發(fā)現(xiàn),“Square_grad” 塊里的 const 節(jié)點(diǎn)值為 2,“gradients” 塊里的 grad_ys_0 值為 1, Shape 值為空,以及 “x” 塊的 const 節(jié)點(diǎn)值為 3。
接下來(lái),我們開始具體分析這個(gè)計(jì)算圖的結(jié)構(gòu)。我們可以注意到,這個(gè)計(jì)算圖的結(jié)構(gòu)是比較清晰的,“x” 塊負(fù)責(zé)變量的讀取和初始化,“Square” 塊負(fù)責(zé)求平方 y = x ^ 2 ,而 “gradients” 塊則負(fù)責(zé)對(duì) “Square” 塊的操作求導(dǎo),即計(jì)算 y_grad = 2 * x。由此我們可以看出, tf.gradients 是一個(gè)相對(duì)比較 “龐大” 的操作,并非如一般的操作一樣往計(jì)算圖中添加了一個(gè)或幾個(gè)節(jié)點(diǎn),而是建立了一個(gè)龐大的子圖,以應(yīng)用鏈?zhǔn)椒▌t求計(jì)算圖中特定節(jié)點(diǎn)的導(dǎo)數(shù)。
在 TensorFlow 2 的圖執(zhí)行模式 API 中,將生成的計(jì)算圖使用 TensorBoard 進(jìn)行展示:
我們可以注意到,除了求導(dǎo)過(guò)程沒(méi)有封裝在 “gradients” 塊內(nèi),以及變量的處理簡(jiǎn)化以外,其他的區(qū)別并不大。由此,我們可以看出,在圖執(zhí)行模式下, tf.GradientTape這一上下文管理器的 gradient 方法和 TensorFlow 1.X 的 tf.gradients 是基本等價(jià)的。小結(jié)
TensorFlow 1.X 中的 tf.gradients 和 TensorFlow 2 圖執(zhí)行模式下的 tf.GradientTape 上下文管理器盡管在 API 層面的調(diào)用方法略有不同,但最終生成的計(jì)算圖是基本一致的。
“哪吒頭”—玩轉(zhuǎn)小潮流
總結(jié)
以上是生活随笔為你收集整理的timertask run函数未执行_图执行模式下的 TensorFlow 2的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 人脸识别拷勤门禁主板_捷易讲解AI无感人
- 下一篇: MATLAB实现SVM多分类(one-v