Rust 的 rusty_machine crate 提供了機器學習演算法的實作,讓開發者能方便地在 Rust 中進行機器學習任務。本文示範如何使用 rusty_machine 建立 K-Means 分群模型和神經網路模型,並說明資料處理、模型訓練和預測的流程。K-Means 演算法透過 UnSupModel 特徵的 train 和 predict 函式進行訓練和預測,並需要事先設定 K 值,也就是預期的群集數量。神經網路模型則需要準備訓練和測試資料,並透過損失函式和梯度下降演算法進行訓練,以調整節點權重並提高預測準確度。程式碼範例展示瞭如何使用 csv crate 讀取和處理 CSV 格式的資料,以及如何使用 gnuplot crate 視覺化分群結果。此外,文章也說明瞭監督式學習中資料生成的重要性,以及如何使用命令列引數傳遞 CSV 檔案路徑,並使用 serde crate 進行資料序列化和反序列化。
K-Means 分群設定
從圖 6-6 可以看出,貓的身形測量資料形成了三個群集。你會預期 K-means 演算法將它們分成三組。K-means 模型位於 rusty_machine::learning::k_means::KMeansClassifier。所有非監督式學習模型,包括 K-means,都實作了 rusty_machine::learning::UnSupModel 特徵。
UnSupModel 特徵
pub trait UnSupModel<T, U> {
fn train(&mut self, inputs: &T) -> LearningResult<()>;
fn predict(&self, inputs: &T) -> LearningResult<U>;
}
train() 函式會接收訓練資料輸入並從中學習。「知識」儲存在模型本身。模型訓練完成後,可以使用 predict() 函式根據從訓練資料中學習到的知識來預測(在此例中為分群)新的資料。
內容解密:
train():接收訓練資料並進行學習,將結果儲存在模型中。predict():利用模型中的知識對新資料進行預測或分群。
在呼叫 train() 之前,需要設定 K-means 中的 K 值。K 代表預期的群集數量。從圖 6-6 可以清楚看出有三個群集,因此應設定 k = 3。如「你正在建構什麼?」一節所述,為 K-means 訓練和分群建立了一個獨立的二進位制檔案:src/bin/cluster.rs。
主要函式實作
extern crate rusty_machine;
use rusty_machine::learning::k_means::KMeansClassifier;
use rusty_machine::learning::UnSupModel;
const CLUSTER_COUNT: usize = 3;
fn main() {
let samples = read_data_from_stdin().unwrap();
let mut model = KMeansClassifier::new(CLUSTER_COUNT);
model.train(&samples).unwrap();
let classes = model.predict(&samples).unwrap();
export_result_to_stdout(samples, classes.into_vec()).unwrap();
}
內容解密:
- 載入 CSV 資料並儲存在
samples中。 - 初始化
KMeansClassifier,設定K = CLUSTER_COUNT(此例中為 3)。 - 使用
model.train()對資料進行訓練,進行分群並儲存中心點。 - 使用
model.predict()對資料進行分群,獲得每個資料點的群集 ID 標籤。 - 將結果輸出到 STDOUT。
資料讀取與輸出
資料讀取函式
fn read_data_from_stdin() -> Result<Matrix<f64>, Box<dyn Error>> {
let mut reader = csv::Reader::from_reader(io::stdin());
let mut data: Vec<f64> = vec![];
for result in reader.records() {
let record = result?;
data.push(record[0].parse().unwrap());
data.push(record[1].parse().unwrap());
}
Ok(Matrix::new(&data.len() / 2, 2, data))
}
內容解密:
- 從 STDIN 使用
csv::Reader讀取 CSV 資料。 - 將讀取到的資料轉換為
Matrix<f64>以供KMeansClassifier.train()使用。
資料輸出函式
fn export_result_to_stdout(samples: Matrix<f64>, classes: Vec<usize>) -> Result<(), Box<dyn Error>> {
let mut writer = csv::Writer::from_writer(io::stdout());
writer.write_record(&["height", "length", "class"])?;
for sample in samples.iter_rows().zip(classes) {
writer.serialize(sample)?;
}
Ok(())
}
內容解密:
- 將原始的 2D 身形測量資料與分群結果結合輸出到 STDOUT。
- 使用
.zip()將兩個矩陣「拼接」在一起輸出。
結果視覺化
use std::error::Error;
use std::io;
use gnuplot::{Figure, Caption, Graph, Color, PointSymbol};
use gnuplot::AxesCommon;
fn main() -> Result<(), Box<dyn Error>> {
let mut x: [Vec<f64>; 3] = [Vec::new(), Vec::new(), Vec::new()];
let mut y: [Vec<f64>; 3] = [Vec::new(), Vec::new(), Vec::new()];
let mut reader = csv::Reader::from_reader(io::stdin());
for result in reader.records() {
let record = result?;
let class: usize = record[2].parse().unwrap();
x[class].push(record[0].parse().unwrap());
y[class].push(record[1].parse().unwrap());
}
let mut fg = Figure::new();
fg.axes2d()
.set_title("Cat breed classification result", &[])
.set_legend(Graph(0.9), Graph(0.1), &[], &[])
.set_x_label("height (cm)", &[])
.set_y_label("length (cm)", &[])
.points(
&x[0],
&y[0],
&[Caption("Cat breed 1"), Color("red"), PointSymbol('+')],
)
.points(
&x[1],
&y[1],
&[Caption("Cat breed 2"), Color("green"), PointSymbol('x')],
)
.points(
&x[2],
&y[2],
&[Caption("Cat breed 3"), Color("blue"), PointSymbol('o')],
);
fg.show();
Ok(())
}
內容解密:
- 將輸入的 CSV 資料根據類別分開儲存到不同的向量中。
- 使用不同的點符號和顏色繪製不同類別的資料點,以便區分。
- 設定圖表標題、坐標軸標籤和圖例。
圖表說明
此圖示展示了不同貓種的分群結果,使用不同的顏色和符號表示不同的類別,有助於視覺化區分各類別之間的差異。
使用類別神經網路辨識貓狗
簡介類別神經網路
在探討無監督學習模型後,接下來要轉向監督學習模型。我們將使用的監督學習模型是人工神經網路(Artificial Neural Network, ANN)模型,或簡稱神經網路。神經網路的靈感來自人類大腦的工作原理。人類大腦由神經元組成,每個神經元接收刺激並決定是否「啟動」。啟動的神經元會向其他相連的神經元傳送電訊號。因此,如果有一個由相互連線的神經元組成的大型網路,它們可以透過調整連線方式和對刺激的敏感度來學習對不同輸入做出反應。
現代神經網路模型保持相同的指導原則,但更側重於使用資料解決經驗性問題,而不是試圖準確地模擬人類大腦。神經網路模型的關鍵元件之一是神經元(或有時稱為節點,如圖 6-9 所示)。一個節點包含一個或多個輸入($x_i$)、它們的權重($w_i$)、一個輸入函式和一個啟動函式。輸入函式對所有輸入進行加權求和,並將結果傳遞給啟動函式。在學習過程中,權重會被調整,以根據輸入訊號對要學習的事物的重要性來放大或減弱訊號。輸入函式的結果將傳遞給啟動函式,以決定是否啟動該節點。
節點結構與工作原理
每個節點根據從前一階段獲得的輸入,結合權重,決定是否應該被啟動。一開始,可以隨機設定權重,但這不會比隨機猜測更好。要從訓練資料中學習,需要根據訓練資料調整節點中的權重。由於我們有真實答案,可以將神經網路的輸出與真實答案進行比較;如果輸出相差甚遠,則意味著需要調整權重,以便下次做出更好的預測。
損失函式與梯度下降
透過損失函式來評估模型目前的表現(好壞)。rusty-machine 預設使用二元交叉熵損失函式,如果輸出(貓或狗)與真實答案不符,則會給出較高的值,反之亦然。目標是調整權重以最小化損失(即,讓損失函式傳回盡可能小的值)。為此,使用了一種稱為梯度下降的演算法。梯度下降會朝著損失減少的方向調整權重。
準備訓練和測試資料
為了保持範例簡單,將使用與 K-means 範例相同的輸入:身高和長度。將為貓和狗各建立 2,000 個樣本。但這裡的資料與 K-means 資料有一些不同:
- 需要為每個樣本提供「答案」或真實標籤。
- 需要生成兩組資料:訓練資料和測試資料。
為何需要真實標籤和測試資料
神經網路是一種監督學習模型,需要透過將其預測與真實答案進行比較來學習並提高其準確性。同時,需要將資料分成訓練集和測試集。訓練集用於訓練模型,而測試集用於驗證訓練好的模型的準確性。關鍵的一點是,在訓練階段,模型絕不能看到測試集中的資料。因為已經有了答案,不能讓模型在訓練期間看到測試資料,否則它就已經知道了答案,可以透過記憶訓練資料快速達到 100% 的準確度。即使演算法並非故意記憶答案,在訓練中使用測試資料通常也會導致過度擬合。過度擬合是指模型試圖過度適應特定的訓練資料集,而無法生成足夠通用的模型。
// 這裡應該有範例程式碼,但根據提供的內容無法直接給出
內容解密:
這段程式碼範例主要展示如何使用 Rust 語言準備訓練和測試資料。雖然具體程式碼未提供,但通常涉及以下步驟:
- 生成樣本資料,包括貓和狗的身高和長度。
- 為每個樣本新增真實標籤。
- 將資料分成訓練集和測試集。
- 使用訓練集來訓練神經網路模型,並使用測試集評估模型的表現。
圖表說明
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Rust機器學習KMeans神經網路模型訓練
package "機器學習流程" {
package "資料處理" {
component [資料收集] as collect
component [資料清洗] as clean
component [特徵工程] as feature
}
package "模型訓練" {
component [模型選擇] as select
component [超參數調優] as tune
component [交叉驗證] as cv
}
package "評估部署" {
component [模型評估] as eval
component [模型部署] as deploy
component [監控維護] as monitor
}
}
collect --> clean : 原始資料
clean --> feature : 乾淨資料
feature --> select : 特徵向量
select --> tune : 基礎模型
tune --> cv : 最佳參數
cv --> eval : 訓練模型
eval --> deploy : 驗證模型
deploy --> monitor : 生產模型
note right of feature
特徵工程包含:
- 特徵選擇
- 特徵轉換
- 降維處理
end note
note right of eval
評估指標:
- 準確率/召回率
- F1 Score
- AUC-ROC
end note
@enduml
此圖示說明瞭神經網路的基本結構,包括輸入層、隱藏層和輸出層,以及訓練資料和測試資料如何與這些層互動。
監督式神經網路模型的資料生成與訓練
在機器學習中,監督式學習是一種常見的訓練模型方法。為了建立一個能夠準確分類別貓狗的模型,我們需要準備訓練資料和測試資料。訓練資料用於訓練模型,而測試資料則用於評估模型的表現。
資料生成
首先,我們需要生成訓練資料和測試資料。資料生成的程式碼結構與前面的範例相似,但generate_data()函式有所不同。清單 6-16 展示瞭如何生成監督式神經網路模型的訓練資料。
清單 6-16:監督式神經網路模型的資料生成函式
use serde::Serialize;
#[derive(Debug, Serialize)]
struct Sample {
height: f64,
length: f64,
category_id: usize,
}
fn generate_data(centroids: &Matrix<f64>, points_per_centroid: usize, noise: f64) -> Vec<Sample> {
// 輸入驗證
let mut samples = Vec::with_capacity(points_per_centroid);
let mut rng = thread_rng();
let normal_rv = Normal::new(0f64, noise).unwrap();
for _ in 0..points_per_centroid {
// 從每個中心點生成點
for (centroid_id, centroid) in centroids.iter_rows().enumerate() {
let mut point = Vec::with_capacity(centroids.cols());
for feature in centroid.iter() {
point.push(feature + normal_rv.sample(&mut rng));
}
samples.push(Sample {
height: point[0],
length: point[1],
category_id: centroid_id,
});
}
}
samples
}
#### 內容解密:
- 定義 Sample 結構:使用
serde::Serialize來序列化資料,結構包含height、length和category_id,分別代表高度、長度和類別標籤。 generate_data函式:根據給定的中心點、每個中心點的點數和雜訊程度生成樣本資料。- 輸入驗證和初始化:檢查輸入引數,初始化樣本向量、亂數生成器和常態分佈亂數變數。
- 生成樣本資料:對每個中心點生成指定數量的點,並加入雜訊,最後將生成的樣本加入
samples向量中。
新生成的資料每一行都有三個欄位:長度、高度和貓或狗的標籤。類別標籤是一個整數,0 代表狗,1 代表貓。
設定神經網路模型
生成訓練和測試資料後,需要建立模型訓練和預測的程式碼。這些程式碼將被放在一個新的二進位制檔案 src/bin/train_and_predict.rs 中。該檔案負責以下任務:
- 讀取和解析訓練資料到
Vec,並轉換成Matrix。 - 對訓練資料進行標準化。
- 初始化神經網路模型。
- 將標準化後的訓練資料輸入模型進行訓練。
- 讀取和解析測試資料,並進行標準化。
- 使用訓練好的模型對測試資料進行預測。
讀取訓練和測試資料
在 K-means 範例中,我們從標準輸入讀取 CSV 資料。然而,在監督式模型中,我們需要兩個輸入檔案:訓練資料和測試資料。因此,這次我們將透過命令列引數傳遞 CSV 檔案路徑,直接從檔案讀取。
清單 6-17:讀取訓練和測試 CSV 檔案的命令列選項
use structopt::StructOpt;
#[derive(StructOpt)]
struct Options {
#[structopt(short = "r", long = "train", parse(from_os_str))]
/// 訓練資料 CSV 檔案
training_data_csv: std::path::PathBuf,
#[structopt(short = "t", long = "test", parse(from_os_str))]
/// 測試資料 CSV 檔案
testing_data_csv: std::path::PathBuf,
}
fn main() -> Result<(), Box<dyn Error>> {
let options = Options::from_args();
// ...
Ok(())
}
#### 內容解密:
- 定義 Options 結構:使用
StructOpt解析命令列引數,包含training_data_csv和testing_data_csv兩個欄位,分別代表訓練資料和測試資料的 CSV 檔案路徑。 main函式:解析命令列引數,將結果存入options變數。
為了反序列化 CSV 資料回 Rust 結構,需要定義相同的資料結構並提供給 csv::Reader。
清單 6-18:讀取訓練資料
extern crate rusty_machine;
use serde::Deserialize;
use csv;
use rusty_machine::linalg::Matrix;
#[derive(Debug, Deserialize)]
struct SampleRow {
height: f64,
length: f64,
category_id: usize,
}
fn read_data_from_csv(file_path: std::path::PathBuf) -> Result<(Matrix<f64>, Matrix<f64>), Box<dyn Error>> {
let mut input_data = vec![];
let mut label_data = vec![];
let mut sample_count = 0;
let mut reader = csv::Reader::from_path(file_path)?;
for raw_row in reader.deserialize() {
let row: SampleRow = raw_row?;
input_data.push(row.height);
input_data.push(row.length);
label_data.push(row.category_id as f64);
sample_count += 1
}
let inputs = Matrix::new(sample_count, 2, input_data);
let targets = Matrix::new(sample_count, 1, label_data);
return Ok((inputs, targets))
}
#### 內容解密:
- 定義 SampleRow 結構:使用
serde::Deserialize反序列化 CSV 資料,結構包含height、length和category_id,分別代表高度、長度和類別標籤。 read_data_from_csv函式:從指定的 CSV 檔案路徑讀取資料,反序列化為SampleRow結構,並將資料轉換成Matrix格式。- 初始化變數:初始化輸入資料向量、標籤資料向量和樣本計數器。
- 讀取 CSV 資料:使用
csv::Reader從檔案路徑讀取 CSV 資料,並反序列化為SampleRow。 - 處理資料:將反序列化的資料存入
input_data和label_data向量,並更新樣本計數器。 - 轉換成 Matrix:將
input_data和label_data向量轉換成Matrix格式,分別代表輸入資料和目標標籤。