The Nature of Code閱讀心得與Python實作:10.5 Building a Gesture...

更新 發佈閱讀 69 分鐘
這一節的標題是
10.5 Building a Gesture Classifier
因為方格子標題字數限制,所以沒完整顯現

這一節要依循機器學習生命週期的步驟來實作一個手勢分類器。透過這個過程,就可以很清楚知道,機器學習生命週期的每一個步驟究竟要做些什麼,以及程式要怎麼寫。

機器學習生命週期的第一步,也就是步驟0,是識別問題,把想做的事、想解決的問題描述清楚。現在,假設我們想製作一套互動系統,使用者可以透過手勢來和這套系統互動。基於這個目的,很顯然的,系統要能識別使用者的手勢,才能知道該怎麼回應使用者。所以,製作一個可以分辨使用者不同手勢的手勢分類器,就成為首要解決的問題。

畢竟這只是個用來教學的例子而已,所以不需要搞得太複雜,就假設這個手勢分類器要分辨的手勢只有簡單的上、下、左、右四種。除了簡化成只需分辨四種手勢之外,我們也以偵測滑鼠的動作來代替偵測手部的動作,這樣就不需要用到其他額外的硬體設備,只要電腦就可以完成這個例子。

那怎麼把滑鼠的動作對應成手勢的方向呢?這其實挺簡單的,透過偵測滑鼠按下、拖曳、放開這三個連續動作,我們可以得到一個向量,而這個向量的方向,就可以當成是手勢的方向。

這裡要注意一下:既然都知道向量的方向了,那就直接判斷這個方向是屬於上、下、左、右哪個方向不就得了,幹嘛還要動用類神經網路來判斷呢?!直接判斷的效率,絕對會高於用類神經網路判斷的呀!

是這樣沒錯啦!不過,我們的目的是想要了解怎麼訓練機器學習模型,越簡單的例子反而越適合,因為越容易確認到底做對了沒有。等到真的都知道怎麼訓練模型之後,再來挑戰複雜的例子也不遲。

Collecting and Preparing the Data

想做的事、想解決的問題都搞清楚之後,接下來是步驟1、2,也就是蒐集、準備資料。

通常蒐集、準備資料的過程會很繁雜。畢竟我們的主要目的是要學習怎麼訓練類神經網路模型,所以就不真正去蒐集資料,而改用如下的合成資料來訓練模型:

dataset = [
{'x': 0.99, 'y': 0.02, 'label': 'right'},
{'x': 0.76, 'y': -0.1, 'label': 'right'},
{'x': -1.0, 'y': 0.12, 'label': 'left'},
{'x': -0.9, 'y': -0.1, 'label': 'left'},
{'x': 0.02, 'y': 0.98, 'label': 'down'},
{'x': -0.2, 'y': 0.75, 'label': 'down'},
{'x': 0.01, 'y': -0.9, 'label': 'up'},
{'x': -0.1, 'y': -0.8, 'label': 'up'}
]

這幾筆寫成dictionary形式的資料,是向量的x、y分量,以及該向量的標籤。向量分量都經過正規化,範圍在-1~1之間。把這些向量及其標籤畫成圖,會長這樣:

vocus|新世代的創作平台

從圖可以看出來,這幾個向量的方向,很容易就可以辨識出是指向上、下、左、右等方向中的哪個方向。

訓練模型用的資料集,通常都會儲存在檔案中,要用的時候再載入,並不會直接寫在程式裡頭。一般常見,用來儲存資料集的格式有JSON和CSV兩種。JSON是JavaScript Object Notation的簡寫,資料儲存方式看起來和dictionary的寫法很像。在python中有個json模組,裡頭提供了各種用來操作JSON檔案的方法。JSON的檔案格式及json模組的詳細用法,可以參考下面這兩篇文章:

至於CSV,則是comma-separated values的簡寫,在python中也有個csv模組,可以用來處理CSV檔案。CSV的檔案格式及csv模組的詳細用法,可以參考下面這兩篇文章:

蒐集資料時有個重點,那就是要蒐集各式各樣具有代表性的例子。真實世界中,人的手勢會有各式各樣不同的變化,在蒐集到的資料中,應該要有能夠充分代表這些變化的例子,這樣訓練出來的模型,面對各種不同變化的手勢時,才有辦法認出那是屬於哪一類的手勢。

Exercise 10.4

不使用人工標註的方式,而直接用極座標的角度來標註手勢方向。

儲存資料時,因為我們只在意向量的方向,所以不儲存原始的向量,而是儲存正規化後的向量。這樣做的好處是,x、y分量的數值都會介於-1~1之間,不會有數值差距很大的情況出現。

# python version 3.13.9
import json
import sys

import pygame # version 2.6.1


def gesture_label(direction):
r, theta = direction.as_polar()
if -45 < theta <= 45:
label = 'right'
elif 45 < theta <= 135:
label = 'down'
elif -135 < theta <= 0:
label = 'up'
else:
label = 'left'

return label


pygame.init()

pygame.display.set_caption("Exercise 10.4")

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)

FPS = 30
frame_rate = pygame.time.Clock()

mouse_pressed = False
save_data = False

start_pt, end_pt = None, None

data = []

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_pressed = True
start_pt = pygame.Vector2(pygame.mouse.get_pos())
end_pt = None
elif event.type == pygame.MOUSEMOTION:
if mouse_pressed:
end_pt = pygame.Vector2(pygame.mouse.get_pos())
elif event.type == pygame.MOUSEBUTTONUP:
mouse_pressed = False
save_data = True

screen.fill(WHITE)

if start_pt is not None and end_pt is not None:
pygame.draw.line(screen, BLACK, start_pt, end_pt, 3)

if save_data:
direction = end_pt - start_pt
if direction.length() > 1.e-6:
# 我們只在意向量的方向,儲存正規化後的向量,各分量的數值
# 不至於有很大的差距
direction.normalize_ip()
data.append({'x': direction.x, 'y': direction.y,
'label': gesture_label(direction)})

try:
with open('./exercise10.4data.json', 'w') as f:
json.dump(data, f)
except Exception as e:
print(f'資料無法存檔: {e}')

save_data = False

pygame.display.update()
frame_rate.tick(FPS)

Choosing a Model

機器學習生命週期的第3個步驟是選擇模型。因為我們要製做的是手勢的分類器,所以就依據設計分類用模型的訣竅來設計模型。

dataset中的資料製作成表格,會得到一個有三個欄位的表格:

x分量 y分量 標籤
0.99 0.02 right
0.76 -0.1 right
: : :

在輸入層部分,扣除標籤,也就是種類欄位,還有兩個欄位,所以,輸入層需要二個神經元。

在隱藏層部分,因為沒什麼可依循的設計訣竅,所以層數和神經元數量就用和原書一樣的,也就是層數是一層,神經元數量是16個。至於啟動函數,原書沒提到,那就用常用的ReLU。

在輸出層部分,因為有右、上、左、下共四個種類的方向,所以需要四個神經元。另外,因為是分類用的模型,所以輸出層的啟動函數用softmax。

根據上述的設計,Keras程式如下:

model = keras.models.Sequential()
model.add(keras.Input(shape=(2,)))
model.add(keras.layers.Dense(units=16, activation='relu'))
model.add(keras.layers.Dense(4, activation='softmax'))

Training the Model

模型設計好之後,再來就是生命週期的第4個步驟,也就是訓練模型。

要訓練模型,就得把資料餵給模型。不過,這模型有點挑嘴,不是什麼樣的資料都願意吃,只有符合特定格式的資料才對它的胃口。所以,在把資料餵給模型之前,得先把資料處理成它願意吃的形式才可以。

要餵給模型的資料有兩部分:輸入資料及目標資料(target data)。目標資料指的是,當餵給模型輸入資料後,我們希望模型能夠輸出的資料。以dataset內的資料所製作成的表格來說,輸入資料就是x分量和y分量這兩個欄位內的資料,而目標資料則是標籤欄位內的資料。

那什麼樣形式的資料模型會吃呢?首先,不管是輸入資料或目標資料,他們的資料型態都必須是Numpy的陣列,或者是元素為Numpy陣列的list。

除了資料型態之外,以dataset中的資料而言,模型願意吃的輸入資料長這樣:

[[ 0.99,  0.02],
[ 0.76, -0.1 ],
[-1. , 0.12],
[-0.9 , -0.1 ],
[ 0.02, 0.98],
[-0.2 , 0.75],
[ 0.01, -0.9 ],
[-0.1 , -0.8 ]]

也就是說,放置輸入資料的陣列或list,其元素必須是一筆一筆資料所構成的陣列。

既然模型很挑嘴,只願意吃特定長相的輸入資料,那我們只好把輸入資料轉換成模型願意吃的長相;程式這樣寫:

x = [[data['x'], data['y']] for data in dataset]
x = numpy.array(x)

輸入資料的處理到這裡就算完成了。

咦?!正規化呢?不是說資料的正規化很重要?怎麼沒看到這個步驟?

資料的正規化可以自己做,也可以用Keras所提供的工具來做。使用Keras所提供的工具當然比較省事,不過也要有正確的觀念,才能讓工具發揮效用。

Keras用來正規化資料的工具稱為「正規化層」(normalization layer)。雖然名稱中有個「層」字,但和隱藏層、輸出層等由神經元所組成的層不同,正規化層並不是由神經元所組成,Keras只是用「層」這個概念來描述、操作它而已。在Keras中,有許多用來預處理(preprocessing)資料的工具都稱為「層」,也都是基於同樣的道理。

要使用Keras的正規化層,首先要先定義它:

normalization_layer = keras.layers.Normalization()

接著要用adapt()方法調整它,把它調成適合用來處理我們的資料的樣子;程式這樣寫:

normalization_layer.adapt(data)

正規化層會去計算data的平均數(mean)和變異數(variance),並認定之後要它正規化的資料,全都具有這樣的統計性質。當有新的資料,假設叫new_data,要正規化時,正規化層就會利用由data處所得到的平均數和變異數,從new_data計算出以0為中心,標準差(standard deviation)為1的方式分佈的資料;這就是new_data正規化後的資料。要將new_data正規化,程式這樣寫:

normalized_data = normalization_layer(new_data)

這樣所得到的normalized_data,就是new_data正規化後的資料。

正規化層可以放在模型外面,也可以整合進模型中。整合進模型中的優點是,正規化層在進行資料正規化時所使用的計算邏輯及參數,全都會成為模型的一部分;換句話說,用adapt()方法調整正規化層時所得到的平均數和變異數,也會成為模型的一部分。因此,當要移植模型到不同的地方時,就不需要再去管正規化層要怎麼正規化資料,就直接把資料餵給模型就可以了。

那要怎麼把正規化層整合進模型中呢?這其實挺簡單的。既然正規化層和輸入層、隱藏層、輸出層一樣,都是「層」,用來操作這些層的做法,也同樣適用於正規化層。例如,要把正規化層加入模型中,可以使用add()方法;程式這樣寫:

add(normalization_layer)

在把正規化層整合進模型中時,有一點要注意,那就是必須把它放在第一層,取代掉輸入層。所以,建造模型的程式會變這樣:

model = keras.models.Sequential()
model.add(normalization_layer)
:
:

正規化層更詳細的使用方法,可以參考官網的說明文件:〈Normalization layer〉

處理好輸入資料之後,接著再來處理目標資料。

目標資料是標籤欄位內的資料,不過這些資料要利用先前提到的獨熱編碼法來改變長相,這樣模型才願意吃。

要針對標籤欄位內的資料進行獨熱編碼,程式可以全部自己寫,不過藉助Keras中的to_categorical()函數來做,會比較省事一些。

在使用to_categorical()函數前,要先把標籤欄位內的資料轉為整數。假設我們設定0、1、2、3分別代表right、up、left、right這些標籤,這樣標籤欄位內的資料就會變成0、0、2、2、3、3、1、1;實作時程式可以這樣寫:

labels = ['right', 'up', 'left', 'down']
y = [data['label'] for data in dataset]
for index, label in enumerate(labels):
y = [index if direction == label else direction for direction in y]

把標籤欄位內的資料轉為整數之後,就可以用to_categorical()函數將其轉成獨熱編碼的形式了;程式這樣寫:

y_one_hot = keras.utils.to_categorical(y, num_classes=4)

因為to_categorical()函數所傳回來的是Numpy的陣列,所以y_one_hot可以直接拿來餵給模型,不需要再進行資料型態的轉換。

到目前為止,我們都是在幫資料整形,讓資料的長相能被模型接受。在機器學習界,有個專門的術語用來指稱資料的長相:形狀(shape)。形狀這個術語,指的是資料的維度和結構,也就是資料要如何按照行、列,甚至更深層的其他維度來排列。機器學習中的資料,通常都會存放在稱為張量(tensor)的資料結構中。簡單來說,張量就是多維的向量,而張量的形狀,就是各個維度的大小。資料要按照正確的方式放進符合模型所要求形狀的張量中,這樣模型才能正確地解讀這些資料的含意。

處理好要餵給模型的資料後,就可以開始訓練模型了。不過,就和老師上課會有個教學計畫一樣,我們也需要擬定模型的訓練計畫。教學計畫裡頭擬定了諸如教學目標、教材教法、評量方式等;模型的訓練計畫裡頭,也會有諸如訓練方法、評量標準等東西。

在Keras中,用來設定模型訓練計畫的是compile()方法;程式這樣寫:

model.compile(optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy']
)

簡單來說,參數optimizer設定的是訓練方法,或者也可以說是模型的學習方法;說得有學問一點,就是最佳化所使用的演算法。參數loss設定的是損失函數,也就是用來衡量距離學習目標有多遠的函數;參數optimizer所設定的最佳化方法,就是要用來找出一組能讓損失函數值達到最小的權重。至於參數metrics所設定的,則是要用哪種計算方式來顯示學習成果

那這些參數在設定時,有沒有什麼訣竅呢?

在損失函數方面,要用categorical_crossentropy。通常如果有用to_categorical()將目標資料轉成獨熱編碼形式,那損失函數用categorical_crossentropy就對了。

metrics參數方面,通常會設定成metrics=['accuracy'],也就是在訓練過程中,顯示資料有被正確分類的比例。要注意的是,這個值並不會被用於訓練模型,單純就只是參考用,這是和損失函數值最大的不同之處。

那如果我們想要設定學習速率,要在哪裡設定呢?要設定學習速率,程式可以這樣寫:

opt = keras.optimizers.Adam(learning_rate=0.005)
model.compile(optimizer=opt,
loss='categorical_crossentropy',
metrics=['accuracy']
)

如果沒有特別設定的話,那就會用預設值。

訓練計畫設定好之後,就可以用fit()方法開始訓練模型了;程式這樣寫:

history = model.fit(x, y_one_hot, epochs=200, verbose=2)

參數epochs設定的是要用多少個訓練期來訓練模型;訓練期和學習速率一樣,都是超參數。參數verbose則是用來設定要怎麼顯示訓練過程;0代表不顯示、1代表顯示資料中包含進度條、2則代表顯示資料中不包含進度條。

fit()方法回傳的是History物件,該物件的history屬性是個dictionary,裡頭記錄了在訓練過程中,模型在每個訓練期結束時之準確度、損失函數值等資料;這些資料在後續評估模型的訓練效果及調校模型的訓練參數時,具有相當重要的參考價值。

到目前為止,一切都很順利,設定好各個參數之後,啟動fit()訓練模型,然後就等結果出來,整個訓練過程都自動完成,完全不需要我們介入。這種全自動的方式雖然挺不賴的,但是卻也有些讓人不滿足。這怎麼說呢?例如,如果訓練到一半程式當掉了,那不就全做白工了?!有沒有可能在訓練時,每隔一段時間就把模型的狀態存起來?如果可以的話,即使程式當掉要重新執行,也可以從中斷的地方繼續訓練下去,不用從頭訓練起。又或者,如果訓練到一半,發現模型已經學得很好了,難道非得要繼續訓練下去,直到整個訓練過程全部結束為止嗎?如果這時能提早結束訓練,不就可以省下很多時間和資源嗎?凡此種種,都是訓練模型時會遇到的狀況,而利用fit()方法中的callbacks選用參數將回呼函數(callback function)傳進fit()中,就能讓我們有效地處理這些狀況。

回呼函數,看著好像是很神秘的存在,但它其實也就是函數而已。回呼函數和一般的函數有個最大的不同點,那就是它會被當成引數傳遞進另一個函數中,等待特定的時機被呼叫執行。在Keras中,有許多現成的回呼函數可以使用,當然也可以依實際上的需要自己製作回呼函數。現成的回呼函數,可以參考官網〈Callbacks API〉這份文件;自己製作回呼函數的方法,則可以參考官網〈Writing your own callbacks〉

原書使用回呼函數的主要目的,是為了要在等待模型訓練完成期間,依然能夠顯示動畫或進行其他操作;這是種非同步作業的方式。然而,對於pygame來說,因為它主迴圈的作業方式本質上是同步的,所以除非利用如多行程(multi-process)、多執行緒(multi-thread)、非同步I/O (asyncio)等技術,否則是無法在pygame中進行非同步作業的。

既然在pygame中很難進行非同步作業,而我們的模型又簡單到很容易就可以訓練好,回呼函數在這裡實在是英雄無用武之地。所以,Keras回呼函數的部分,就此打住。

Evaluating the Model

模型訓練好之後,接下來是生命週期的第5個步驟,也就是評估模型。

先前提到,fit()方法回傳的物件中,記錄了模型在訓練過程中的準確度、損失函數值等資料。現在就來看看,到底在模型的訓練過程中,準確度和損失函數值是怎麼變化的。

fit()方法所傳回物件的history屬性是個dictionary,這個dictionary有'accuracy''loss'兩個key;這兩個key的value皆為1D的list,而list的元素,就是每個訓練期結束時之準確度、損失函數值。用200個訓練期以及0.005的學習速率來訓練模型,將準確度、損失函數值對訓練期作圖,可以得到下列圖形:

vocus|新世代的創作平台

先來看損失函數值的部分。從圖可以看出來,在剛開始訓練時,模型的損失函數值很大,因為這時模型還沒學到什麼東西。隨著訓練期的增加,損失函數值快速降低,而在超過100個訓練期後,下降的速度減慢很多,代表模型已經學到很多了,所以進步有限;這就好比很容易可以從0分進步到50分,但要從98分進步到99分,那可就沒那麼容易了。在理想狀況下,越多的訓練期會讓損失函數值降得越低,也就是模型會學得越好;這在訓練模型時,是個好現象。

在這裡,我們用了200個訓練期來訓練模型,這數量其實有點多,不過那是因為我們的資料量相當少的緣故。在實務上,用來訓練模型的資料量通常都不少,所以使用的訓練期數量不會這麼多。

現在來看準確度的部分。從圖可以看出來,訓練不到25個訓練期,模型的準確度就已經達到1.0了;這還真是成效斐然啊!

先別高興得太早,這訓練結果看起來相當好,但卻也潛藏著問題。什麼樣的問題呢?先前在介紹機器學習的生命週期時提到,在準備資料時,會將資料切分成三個部分:

  • 訓練資料:用來訓練模型的主要資料。
  • 驗證資料:不用於訓練模型,只在訓練期間用於檢查模型;檢查通常會在每個訓練期結束時進行。
  • 測試資料:訓練過程中完全沒用到的資料;當訓練完成之後,用來確認模型之最終表現。

但現在我們並沒有這麼做,而是把所有資料都當作訓練資料,一股腦兒地拿來訓練模型;誰叫我們就只有八筆資料,這麼少的資料量,實在是沒辦法切啊!

所以,雖然訓練結果看起來相當好,但由於沒有經過驗證、測試,當模型碰到完全沒有見過的資料時,它的表現是不是會像訓練時這麼好,實在是不無疑問;因為模型有可能過度擬合(overfitting)。

過度擬合也有翻譯成過擬合的,它指的是,模型在面對訓練資料時表現得非常好,但面對完全沒有見過的資料時,卻表現得非常差。之所以會出現過度擬合的現象,用通俗一點的方式來說,就是模型只是死記、死背訓練資料,並不是真的從訓練資料學到可以活用的東西。所以當面對從沒看過的東西時,表現自然不會好到哪裡去。

在訓練模型時,應該要避免過度擬合。那我們怎麼知道模型過度擬合了呢?這時驗證資料就可以派上用場了。使用驗證資料的主要目的,就是要在訓練過程中監測模型的學習成效。當在訓練過程中,模型在訓練資料方面的準確度上升,但在驗證資料方面的準確度卻下降,這種不一致的情況,極有可能就是過度擬合所造成的。

關於過度擬合,可以參考下面這篇文章,裡頭有非常詳細又淺顯易懂的說明:

知道驗證資料在訓練模型時所擔任的角色之後,現在就來看看,在寫Keras程式訓練模型時,要怎麼加入驗證資料。

在Keras中,可以透過設定fit()方法的參數來加入驗證資料。設定方式有兩種,一種是從訓練資料中切分一定比例的資料當作驗證資料,另一種則是直接輸入驗證資料。如果要從訓練資料中切分出驗證資料,可以透過參數validation_split來設定。validation_split是個0~1之間的浮點數,代表要切分多少比例的訓練資料作為驗證資料。如果想直接輸入驗證資料,則可以透過參數validation_data來輸入;資料的格式詳見官網的〈Model training APIs〉這份說明文件。這兩個參數在使用上要注意的是,一次只能用一種,如果兩種都設定了,那就只有validation_data會有作用。

先前提到過,fit()方法會傳回History物件,其history屬性內存有模型在訓練過程中的準確度、損失函數值等資料。當有驗證資料存在時,這個history屬性內會多出'val_accuracy''val_loss'這兩個value皆為1D list的key,而list的元素,就是每個訓練期結束時,使用驗證資料所得到的模型準確度及損失函數值。

訓練資料和驗證資料都用上了,那怎麼用測試資料來評估模型的訓練成果呢?

要用測試資料來憑估模型的訓練成果,方式很簡單,就是把測試資料丟給evaluate()方法,讓它去計算模型使用測試資料時之準確度及損失函數值;程式這樣寫:

result = model.evaluate(x_test, y_test, verbose=2)

程式中的x_testy_test是測試資料,其格式和訓練資料xy_one_hot的格式是相同的。傳回值,也就是result,是個list,其元素就是準確度及損失函數值。

Tuning the Parameters

模型訓練完之後,通常會因為評估結果不盡理想而需要調整超參數並重新訓練,藉此來讓模型能有最好的效能;這個過程可能需要重複多次才能得到滿意的結果。

在Keras中也有用來協助調校參數的工具,詳細的使用方式可以參考官網的說明:

那我們的模型還需要調校嗎?從先前的模型訓練過程圖形可以看到:準確度已達到1.0,而損失函數值也一路下降到低於0.1;所以,模型的表現已經夠好了,不需要再調校了。

Deploying the Model

模型訓練好了,接下來就是部署模型。

部署模型通常會涉及將模型整合到其他應用程式中,以便用來針對新的、從未見過的資料進行預測或決策。在Keras中,可以使用save()方法來將模型存檔,如果要載入存在檔案中的模型,則使用load_model()函數;利用這兩個工具,就可以將訓練好的模型移植到不同的地方,不需要每移植到一個地方就要從頭訓練起。

模型部署好之後,就可以用來針對輸入的資料進行預測。在Keras中,可以用predict()方法來預測輸入的資料會得到怎樣的輸出。使用predict()方法時要注意的是,輸入資料的形狀必須和訓練資料的形狀一樣;至於輸出的形狀,則會和目標資料的形狀一樣。所以,如果要讓模型預測(1, 0)這個向量的輸出,那程式要這樣寫:

input_data = numpy.array([[1, 0]])
output = model.predict(input_data)

因為訓練模型用的目標資料是經過獨熱編碼法編碼所得到的numpy陣列,所以用predict()方法所得到的輸出,也會是和目標資料有相同形狀的numpy陣列,而不是真正的標籤。例如,使用我們訓練好的模型來預測向量(1, 0)的輸出,會得到像這樣的numpy陣列:

array([[9.9227953e-01, 2.7113236e-03, 5.2673812e-04, 4.4823857e-03]], dtype=float32)

陣列的元素是對應於各個標籤的機率值,代表模型認為輸入資料會對應到該標籤的機率;數值越高,代表機率越大。先前提到過,設計分類用的模型時,輸出層神經元的啟動函數要用softmax,從上述的輸出就可以看出道理何在。上述輸出所有元素值的總和是1,而其中有個元素值特別大,其餘的則特別小。所以,只要找出數值最大的那個元素所對應的標籤,就會是模型所預測的標籤。要達到這個目的,程式可以這樣寫:

labels = ['right', 'up', 'left', 'down']
index = numpy.argmax(output)
label = labels[index]

這裡要注意的是,在labels這個list裡頭標籤的排列順序,必須要和目標資料進行獨熱編碼法時所用的順序一致;原先是怎麼個排列法,現在也要是那麼個排列法。

接下來,就來部署這個訓練好的手勢分類器模型,用它來辨識使用者手勢的方向。

通常模型都會部署在訓練環境之外的其他的地方,不過在這裡,我們就直接把訓練好的模型部署在訓練環境所在的地方。這樣的說法看起來很有學問,不過說穿了,就只是把所有的程式都寫在同一個檔案中而已啦!

Example 10.2: Gesture Classifier

訓練過程中,各個訓練期之準確度、損失函數值之變化如下圖所示:

vocus|新世代的創作平台

訓練完成,可以開始識別使用者手勢的方向。

vocus|新世代的創作平台

滑鼠由左下方拖曳至右上方,模型正確辨識出手勢方向為「right」。

vocus|新世代的創作平台

程式如下:

# python version 3.13.9
import sys

import keras # version 3.11.2
import matplotlib.pyplot as plt # version 3.10.0
import numpy # version 2.1.3
import pygame # version 2.6.1


# 要使用的訓練期數量
epochs = 200

# 訓練用的資料集
dataset = [
{'x': 0.99, 'y': 0.02, 'label': 'right'},
{'x': 0.76, 'y': -0.1, 'label': 'right'},
{'x': -1.0, 'y': 0.12, 'label': 'left'},
{'x': -0.9, 'y': -0.1, 'label': 'left'},
{'x': 0.02, 'y': 0.98, 'label': 'down'},
{'x': -0.2, 'y': 0.75, 'label': 'down'},
{'x': 0.01, 'y': -0.9, 'label': 'up'},
{'x': -0.1, 'y': -0.8, 'label': 'up'}
]

# 輸入資料
x = [[data['x'], data['y']] for data in dataset]
x = numpy.array(x)

# 目標資料
# 0: right, 1: up, 2: left, 3: right
labels = ['right', 'up', 'left', 'down']
y = [data['label'] for data in dataset]
for index, label in enumerate(labels):
y = [index if direction == label else direction for direction in y]

y_one_hot = keras.utils.to_categorical(y, num_classes=4)

# 建立並訓練模型
# 重置Keras狀態,避免先前的狀態影響此次的訓練
keras.backend.clear_session()

normalization_layer = keras.layers.Normalization()
normalization_layer.adapt(x)

model = keras.models.Sequential()
model.add(normalization_layer)
model.add(keras.layers.Dense(units=16, activation='relu'))
model.add(keras.layers.Dense(4, activation='softmax'))

opt = keras.optimizers.Adam(learning_rate=0.005)
model.compile(optimizer=opt,
loss='categorical_crossentropy',
metrics=['accuracy']
)

history = model.fit(x, y_one_hot, epochs=epochs, verbose=2)

# 繪製各個訓練期之準確度、損失函數值變化圖
fig, ax = plt.subplots()
ax.plot(history.history['accuracy'], linewidth=1, label='accuracy')
ax.plot(history.history['loss'], linewidth=1, label='loss')

ax.set_xlim(left=0, right=epochs)
ax.set_ylim(bottom=0)

ax.set_xlabel('epoch')
ax.set_ylabel('loss')

plt.legend()
plt.show()

# 將訓練好的模型整合至使用環境中
pygame.init()

pygame.display.set_caption("Example 10.2: Gesture Classifier")

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)

FPS = 30
frame_rate = pygame.time.Clock()

font = pygame.font.SysFont('courier', 32)
string = 'ready'

mouse_pressed = False
generate_prediction = False

start_pt, end_pt = None, None

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_pressed = True
start_pt = pygame.Vector2(pygame.mouse.get_pos())
end_pt = None
elif event.type == pygame.MOUSEMOTION:
if mouse_pressed:
end_pt = pygame.Vector2(pygame.mouse.get_pos())
elif event.type == pygame.MOUSEBUTTONUP:
mouse_pressed = False
generate_prediction = True

screen.fill(WHITE)

if start_pt is not None and end_pt is not None:
pygame.draw.line(screen, BLACK, start_pt, end_pt, 3)

# 辨識使用者手勢的方向
if generate_prediction:
direction = end_pt - start_pt
if direction.length() > 1.e-6:
direction.normalize_ip()
input_data = numpy.array([[direction.x, direction.y]])
output = model.predict(input_data, verbose=0)
index = numpy.argmax(output)
string = labels[index]

generate_prediction = False

text = font.render(string, True, (0, 0, 0))
text_rect = text.get_rect(center=(WIDTH/2, HEIGHT/2))
screen.blit(text, text_rect)

pygame.display.update()
frame_rate.tick(FPS)

Exercise 10.5

蒐集資料部分,見Exercise 10.4。

訓練模型部分,程式如下:

# python version 3.13.9
import json

import keras # version 3.11.2
import matplotlib.pyplot as plt # version 3.10.0
import numpy # version 2.6.1


# 要使用的訓練期數量
epochs = 200

# 載入資料集
try:
with open('./exercise10.4data.json', 'r') as f:
dataset = json.load(f)
except Exception as e:
print(f'資料無法讀取: {e}')

# 輸入資料
x = [[data['x'], data['y']] for data in dataset]
x = numpy.array(x)

# 目標資料
# 0: right, 1: up, 2: left, 3: right
labels = ['right', 'up', 'left', 'down']
y = [data['label'] for data in dataset]
for index, label in enumerate(labels):
y = [index if direction == label else direction for direction in y]

y_one_hot = keras.utils.to_categorical(y, num_classes=4)

# 建立並訓練模型
# 重置Keras狀態,避免先前的狀態影響此次的訓練
keras.backend.clear_session()

normalization_layer = keras.layers.Normalization()
normalization_layer.adapt(x)

model = keras.models.Sequential()
model.add(normalization_layer)
model.add(keras.layers.Dense(units=16, activation='relu'))
model.add(keras.layers.Dense(4, activation='softmax'))

opt = keras.optimizers.Adam(learning_rate=0.005)
model.compile(optimizer=opt,
loss='categorical_crossentropy',
metrics=['accuracy']
)

history = model.fit(x, y_one_hot, epochs=epochs, verbose=2)

# 繪製各個訓練期之準確度、損失函數值變化圖
fig, ax = plt.subplots()
ax.plot(history.history['accuracy'], linewidth=1, label='accuracy')
ax.plot(history.history['loss'], linewidth=1, label='loss')

ax.set_xlim(left=0, right=epochs)
ax.set_ylim(bottom=0)

ax.set_xlabel('epoch')
ax.set_ylabel('loss')

plt.legend()
plt.show()

# 將訓練好的模型存檔
try:
model.save('./exercise10.5model.keras')
except Exception as e:
print(f'模型存檔失敗:{e}')

部署模型部分,程式如下:

# python version 3.13.9
import sys

import keras # version 3.11.2
import numpy # version 2.1.3
import pygame # version 2.6.1


def get_label(index):
labels = ['right', 'up', 'left', 'down']
return labels[index]


# 重置Keras狀態,避免先前的狀態影響目前的工作
keras.backend.clear_session()

# 載入模型
model = keras.saving.load_model('./exercise10.5model.keras')

# 將模型整合至使用環境中
pygame.init()

pygame.display.set_caption("Exercise 10.5-3")

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)

FPS = 30
frame_rate = pygame.time.Clock()

font = pygame.font.SysFont('courier', 32)
string = 'ready'

mouse_pressed = False
generate_prediction = False

start_pt, end_pt = None, None

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_pressed = True
start_pt = pygame.Vector2(pygame.mouse.get_pos())
end_pt = None
elif event.type == pygame.MOUSEMOTION:
if mouse_pressed:
end_pt = pygame.Vector2(pygame.mouse.get_pos())
elif event.type == pygame.MOUSEBUTTONUP:
mouse_pressed = False
generate_prediction = True

screen.fill(WHITE)

if start_pt is not None and end_pt is not None:
pygame.draw.line(screen, BLACK, start_pt, end_pt, 3)

# 辨識使用者手勢的方向
if generate_prediction:
direction = end_pt - start_pt
if direction.length() > 1.e-6:
direction.normalize_ip()
input_data = numpy.array([[direction.x, direction.y]])
output = model.predict(input_data, verbose=0)
index = numpy.argmax(output)
string = get_label(index)

generate_prediction = False

text = font.render(string, True, (0, 0, 0))
text_rect = text.get_rect(center=(WIDTH/2, HEIGHT/2))
screen.blit(text, text_rect)

pygame.display.update()
frame_rate.tick(FPS)

Exercise 10.6

偵測滑鼠移動的軌跡,並將其切分成三段。由每段軌跡的頭、尾兩點可以算出一個向量,所以每條軌跡可以得到三個向量;這就是輸入資料。至於目標資料,也就是軌跡的標籤,不用人工標註的方式取得,而直接使用由軌跡頭、尾兩點所形成的向量之方向。

偵測滑鼠移動軌跡時,座標點的數量要超過10點才算是有效的軌跡。所以,pygame的FPS不要設得太低,免得滑鼠移動得比較快時,抓到的點數數量太少。

vocus|新世代的創作平台
vocus|新世代的創作平台

蒐集資料部分,程式如下:

# python version 3.13.9
import json
import sys

import pygame # version 2.6.1


def gesture_label(points):
direction = points[-1] - points[0]
r, theta = direction.as_polar()
if -45 < theta <= 45:
label = 'right'
elif 45 < theta <= 135:
label = 'down'
elif -135 < theta <= 0:
label = 'up'
else:
label = 'left'

return label


pygame.init()

pygame.display.set_caption("Exercise 10.6-1")

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)

FPS = 60
frame_rate = pygame.time.Clock()

mouse_pressed = False
mouse_dragged = False
save_data = False

data = []

points = []

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_pressed = True
points = []
elif event.type == pygame.MOUSEMOTION:
if mouse_pressed:
mouse_dragged = True
elif event.type == pygame.MOUSEBUTTONUP:
mouse_pressed = False
mouse_dragged = False
save_data = True

screen.fill(WHITE)

if mouse_dragged:
points.append(pygame.Vector2(pygame.mouse.get_pos()))

if points:
for point in points:
pygame.draw.circle(screen, BLACK, point, 1)

if save_data:
if len(points) > 10:
n = len(points)//3
vec1 = points[n] - points[0]
vec2 = points[2*n] - points[n]
vec3 = points[-1] - points[2*n]
vecs = [vec1, vec2, vec3]
for vec in vecs:
if vec.length() > 1.e-6:
vec.normalize_ip()

# pygame向量無法存入json中,需轉成list
vecs_json = [list(vec) for vec in vecs]
data.append({'vectors': vecs_json, 'label': gesture_label(points)})

try:
with open('./exercise10.6data.json', 'w') as f:
json.dump(data, f)
except Exception as e:
print(f'資料存檔失敗:{e}')

save_data = False

pygame.display.update()
frame_rate.tick(FPS)

訓練模型部分,程式如下:

# python version 3.13.9
import json

import keras # version 3.11.2
import matplotlib.pyplot as plt # version 3.10.0
import numpy # version 2.1.3


# 要使用的訓練期數量
epochs = 200

# 載入資料集
try:
with open('./exercise10.6data.json', 'r') as f:
dataset = json.load(f)
except Exception as e:
print(f'資料無法讀取: {e}')

# 輸入資料
x = []
for item in dataset:
vecs = item['vectors']
components = [component for vec in vecs for component in vec]
x.append(components)

x = numpy.array(x)

# 目標資料
# 0: right, 1: up, 2: left, 3: right
labels = ['right', 'up', 'left', 'down']
y = [data['label'] for data in dataset]
for index, label in enumerate(labels):
y = [index if direction == label else direction for direction in y]

y_one_hot = keras.utils.to_categorical(y, num_classes=4)

# 建立並訓練模型
# 重置Keras狀態,避免先前的狀態影響此次的訓練
keras.backend.clear_session()

normalization_layer = keras.layers.Normalization()
normalization_layer.adapt(x)

model = keras.models.Sequential()
model.add(normalization_layer)
model.add(keras.layers.Dense(units=16, activation='relu'))
model.add(keras.layers.Dense(4, activation='softmax'))

opt = keras.optimizers.Adam(learning_rate=0.005)
model.compile(optimizer=opt,
loss='categorical_crossentropy',
metrics=['accuracy']
)

history = model.fit(x, y_one_hot, epochs=epochs, verbose=2)

# 繪製各個訓練期之準確度、損失函數值變化圖
fig, ax = plt.subplots()
ax.plot(history.history['accuracy'], linewidth=1, label='accuracy')
ax.plot(history.history['loss'], linewidth=1, label='loss')

ax.set_xlim(left=0, right=epochs)
ax.set_ylim(bottom=0)

ax.set_xlabel('epoch')
ax.set_ylabel('loss')

plt.legend()
plt.show()

# 將訓練好的模型存檔
try:
model.save('./exercise10.6model.keras')
except Exception as e:
print(f'模型存檔失敗:{e}')

部署模型部分,程式如下:

# python version 3.13.9
import sys

import keras # version 3.11.2
import numpy # version 2.1.3
import pygame # version 2.6.1


def get_label(index):
labels = ['right', 'up', 'left', 'down']
return labels[index]


# 重置Keras狀態,避免先前的狀態影響目前的工作
keras.backend.clear_session()

# 載入模型
model = keras.saving.load_model('./exercise10.6model.keras')

# 將模型整合至使用環境中
pygame.init()

pygame.display.set_caption("Exercise 10.6-3")

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)

FPS = 60
frame_rate = pygame.time.Clock()

font = pygame.font.SysFont('courier', 32)
string = 'ready'

mouse_pressed = False
mouse_dragged = False
generate_prediction = False

data = []

points = []

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_pressed = True
points = []
elif event.type == pygame.MOUSEMOTION:
if mouse_pressed:
mouse_dragged = True
elif event.type == pygame.MOUSEBUTTONUP:
mouse_pressed = False
mouse_dragged = False
generate_prediction = True

screen.fill(WHITE)

if mouse_dragged:
points.append(pygame.Vector2(pygame.mouse.get_pos()))

if points:
for point in points:
pygame.draw.circle(screen, BLACK, point, 1)

# 辨識使用者手勢的方向
if generate_prediction:
if len(points) > 10:
n = len(points)//3
vec1 = points[n] - points[0]
vec2 = points[2*n] - points[n]
vec3 = points[-1] - points[2*n]
vecs = [vec1, vec2, vec3]
for vec in vecs:
if vec.length() > 1.e-6:
vec.normalize_ip()

x = [component for vec in vecs for component in vec]
input_data = numpy.array([x])
output = model.predict(input_data, verbose=0)
index = numpy.argmax(output)
string = get_label(index)
else:
string = 'Not enough data points.'

generate_prediction = False

text = font.render(string, True, (0, 0, 0))
text_rect = text.get_rect(center=(WIDTH/2, HEIGHT/2))
screen.blit(text, text_rect)

pygame.display.update()
frame_rate.tick(FPS)

Exercise 10.7


留言
avatar-img
ysf的沙龍
24會員
168內容數
寫點東西自娛娛人
ysf的沙龍的其他內容
2026/02/23
本節介紹機器學習的生命週期,以及如何用Keras建立用於分類和迴歸的模型。
Thumbnail
2026/02/23
本節介紹機器學習的生命週期,以及如何用Keras建立用於分類和迴歸的模型。
Thumbnail
2026/02/02
單一一個感知器雖然能解一些問題,但能解的問題範圍極其有限。如果把多個感知器組合,形成一個多層感知器(multilayer perceptron, MLP),那這個由多個神經元所組成的網路,將可以解決更複雜、更困難的問題。這一節的主要內容,就在概略地介紹多層感知器。
Thumbnail
2026/02/02
單一一個感知器雖然能解一些問題,但能解的問題範圍極其有限。如果把多個感知器組合,形成一個多層感知器(multilayer perceptron, MLP),那這個由多個神經元所組成的網路,將可以解決更複雜、更困難的問題。這一節的主要內容,就在概略地介紹多層感知器。
Thumbnail
2026/01/26
這節要介紹的是最簡單的類神經網路,也就是只含有一個神經元的感知器(perceptron)。
Thumbnail
2026/01/26
這節要介紹的是最簡單的類神經網路,也就是只含有一個神經元的感知器(perceptron)。
Thumbnail
看更多
你可能也想看
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
ETL是資料倉儲領域中一個重要的概念,全稱為Extract-Transform-Load,中文可譯為"抽取-轉換-載入"。ETL的作用是將來自不同來源的資料抽取出來,經過清理、轉換、整合等處理後,最終將處理好的資料載入到資料倉儲或其他單一的資料存放區
Thumbnail
ETL是資料倉儲領域中一個重要的概念,全稱為Extract-Transform-Load,中文可譯為"抽取-轉換-載入"。ETL的作用是將來自不同來源的資料抽取出來,經過清理、轉換、整合等處理後,最終將處理好的資料載入到資料倉儲或其他單一的資料存放區
Thumbnail
另外站長打個廣告,最近站長正在嘗試經營遊戲直播平台希望大家能夠幫忙追隨訂閱一下,站長真心感謝~ TWITCH直播: https://www.twitch.tv/saioyan Youtube: https://www.youtube.com/channel/UCtCeeanvsVdA
Thumbnail
另外站長打個廣告,最近站長正在嘗試經營遊戲直播平台希望大家能夠幫忙追隨訂閱一下,站長真心感謝~ TWITCH直播: https://www.twitch.tv/saioyan Youtube: https://www.youtube.com/channel/UCtCeeanvsVdA
Thumbnail
關鍵字:python、 Jit、Just-in-time compilation、直譯、編譯、加速、運算 即時編譯Jit(Just-in-time compilation)是一種提高程式執行效率的方法,結合靜態和動態編譯改善了直譯器的效能 Linux的下載安裝: pip install n
Thumbnail
關鍵字:python、 Jit、Just-in-time compilation、直譯、編譯、加速、運算 即時編譯Jit(Just-in-time compilation)是一種提高程式執行效率的方法,結合靜態和動態編譯改善了直譯器的效能 Linux的下載安裝: pip install n
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
背景:從冷門配角到市場主線,算力與電力被重新定價   小P從2008進入股市,每一個時期的投資亮點都不同,記得2009蘋果手機剛上市,當時蘋果只要在媒體上提到哪一間供應鏈,隔天股價就有驚人的表現,當時光學鏡頭非常熱門,因為手機第一次搭上鏡頭可以拍照,也造就傳統相機廠的殞落,如今手機已經全面普及,題
Thumbnail
一個被蚊子激怒的夏夜,催生了我人生第一個程式專案!本文紀錄一個程式新手,如何靠著一股怨氣與勇氣,用Python打造出懷舊的「滅蚊大進擊」遊戲。分享從零到一的真實挑戰、充滿血淚的學習心得,以及最終戰勝BUG的喜悅。
Thumbnail
一個被蚊子激怒的夏夜,催生了我人生第一個程式專案!本文紀錄一個程式新手,如何靠著一股怨氣與勇氣,用Python打造出懷舊的「滅蚊大進擊」遊戲。分享從零到一的真實挑戰、充滿血淚的學習心得,以及最終戰勝BUG的喜悅。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
關鍵字:python、 Cython、Model、C語言、加速、編譯、腳本、模組 Cython是將python轉換成C語言後執行,據說程式在C環境裡面執行速度高於python 以下就在Linux的作業系統下示範 先從下載安裝開始 使用pip3安裝Cython pip3 instal
Thumbnail
關鍵字:python、 Cython、Model、C語言、加速、編譯、腳本、模組 Cython是將python轉換成C語言後執行,據說程式在C環境裡面執行速度高於python 以下就在Linux的作業系統下示範 先從下載安裝開始 使用pip3安裝Cython pip3 instal
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
這是一場修復文化與重建精神的儀式,觀眾不需要完全看懂《遊林驚夢:巧遇Hagay》,但你能感受心與土地團聚的渴望,也不急著在此處釐清或定義什麼,但你的在場感受,就是一條線索,關於如何找著自己的路徑、自己的聲音。
Thumbnail
另外站長打個廣告,最近站長正在嘗試經營遊戲直播平台希望大家能夠幫忙追隨訂閱一下,站長真心感謝~ TWITCH直播: https://www.twitch.tv/saioyan Youtube: https://www.youtube.com/channel/UCtCeeanvsVdAuqNU
Thumbnail
另外站長打個廣告,最近站長正在嘗試經營遊戲直播平台希望大家能夠幫忙追隨訂閱一下,站長真心感謝~ TWITCH直播: https://www.twitch.tv/saioyan Youtube: https://www.youtube.com/channel/UCtCeeanvsVdAuqNU
Thumbnail
前言: 在前面的文裡講過,之前交付給工廠的程式都屬於原始碼,但是今天假設不想公開原始碼給其他人的話或是你要交付的對象是客戶,然後又礙於公司的一些規定所以不能給原始碼的時候,其實可以交付pyc檔。但在現今火箭都上外太空了,如果對方有心想要破解的話其實網路想也有很多教導反組譯的方式。 使用說明
Thumbnail
前言: 在前面的文裡講過,之前交付給工廠的程式都屬於原始碼,但是今天假設不想公開原始碼給其他人的話或是你要交付的對象是客戶,然後又礙於公司的一些規定所以不能給原始碼的時候,其實可以交付pyc檔。但在現今火箭都上外太空了,如果對方有心想要破解的話其實網路想也有很多教導反組譯的方式。 使用說明
Thumbnail
另外站長打個廣告,最近站長正在嘗試經營遊戲直播平台希望大家能夠幫忙追隨訂閱一下,站長真心感謝~ TWITCH直播: https://www.twitch.tv/saioyan Youtube: https://www.youtube.com/channel/UCtCeeanvsVdAuqNU
Thumbnail
另外站長打個廣告,最近站長正在嘗試經營遊戲直播平台希望大家能夠幫忙追隨訂閱一下,站長真心感謝~ TWITCH直播: https://www.twitch.tv/saioyan Youtube: https://www.youtube.com/channel/UCtCeeanvsVdAuqNU
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News