Rust 的 ndarray 和 csv crate 提供了便捷的工具,方便我們進行資料處理和分析。本文將示範如何使用這些工具讀取 CSV 格式資料、執行 K-Means 分群演算法,並使用 Plotters 函式庫繪製分群結果。此外,我們也將探討如何建立一個簡單的神經網路模型,並使用 Rust 的機器學習函式庫 rusty-machine 進行訓練和預測。過程中,我們會使用 rand 和 rand_distr crate 生成模擬的貓狗資料,包含身高、長度和類別標籤,用於訓練和測試神經網路模型。為了評估模型的效能,我們會將資料集分成訓練集和測試集,並使用測試集來驗證模型的泛化能力,避免過度擬合問題。
K-Means 分群演算法實作與視覺化分析
在前述章節中,我們已經探討了 K-Means 分群演算法的基本原理及其在 Rust 語言中的實作細節。本章節將進一步深入討論如何透過程式碼實作資料讀取、分群結果輸出以及視覺化呈現。
資料讀取與轉換
首先,我們需要從標準輸入讀取資料並將其轉換為 Array2 格式,以便進行 K-Means 分群運算。以下程式碼展示瞭如何實作這一過程:
use std::io;
use ndarray::Array2;
fn read_data_from_stdin() -> Result<Array2<f64>, Box<dyn Error>> {
let mut points: Vec<f64> = Vec::new();
let mut reader = csv::Reader::from_reader(io::stdin());
for result in reader.records() {
let record = result?;
points.push(record[0].parse()?);
points.push(record[1].parse()?);
}
let rows = points.len() / 2;
let cols = 2;
Ok(Array2::from_shape_vec((rows, cols), points)?)
}
內容解密:
- 使用
csv::Reader從標準輸入讀取 CSV 格式的資料。 - 將讀取的資料解析為
f64型別並儲存在points向量中。 - 根據
points的長度計算資料的行數和列數,並將其轉換為Array2格式。
分群結果輸出
接下來,我們需要將分群結果輸出到標準輸出。以下程式碼展示瞭如何實作這一過程:
use ndarray::Array1;
fn export_result_to_stdout(
points: Array2<f64>,
classes: Array1<usize>,
) -> Result<(), Box<dyn Error>> {
let mut writer = csv::Writer::from_writer(io::stdout());
writer.write_record(&["height", "length", "class"])?;
for (point, class) in points.rows().into_iter().zip(classes.into_iter()) {
let mut row_iter = point.into_iter();
writer.serialize((
row_iter.next().unwrap(),
row_iter.next().unwrap(),
class
))?;
}
Ok(())
}
內容解密:
- 使用
csv::Writer將分群結果寫入到標準輸出。 - 將原始資料點與其對應的分群類別結合,並寫入 CSV 檔案中。
- 使用
.zip()方法將資料點與類別進行配對。
分群結果視覺化
最後,我們需要將分群結果視覺化,以便更直觀地觀察分群效果。以下程式碼展示瞭如何使用 plotters 函式庫實作視覺化:
use plotters::prelude::*;
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()?;
x[class].push(record[0].parse()?);
y[class].push(record[1].parse()?);
}
let root_drawing_area = BitMapBackend::new("k-means-result-plot.png", (900, 600))
.into_drawing_area();
root_drawing_area.fill(&WHITE)?;
let mut chart = ChartBuilder::on(&root_drawing_area)
.caption("Cat body measurements", ("sans-serif", 30))
.x_label_area_size(30)
.y_label_area_size(40)
.build_cartesian_2d(15.0..45.0, 30.0..55.0)?;
chart.configure_mesh()
.x_desc("height (cm)")
.y_desc("width (cm)")
.disable_mesh()
.draw()?;
// 繪製不同類別的資料點
chart.draw_series(x[0].iter().zip(y[0].iter()).map(|point| {
Cross::new((*point.0, *point.1), 3, Into::<ShapeStyle>::into(&BLUE).stroke_width(2))
}))?;
// ... 其他類別的繪製程式碼 ...
Ok(())
}
內容解密:
- 使用
plotters函式庫建立一個新的繪圖區域。 - 將資料點按照類別進行分類別,並儲存在
x和y向量中。 - 使用不同的符號和顏色繪製不同類別的資料點。
使用神經網路偵測貓與狗的區別
在撰寫了一個簡單的K-means無監督學習模型後,我們現在將轉向一個更複雜的神經網路作為我們的監督學習模型。神經網路遠比K-means複雜,但幸運的是,由於Rust函式庫的實用性,我們可以跳過細節,直接將程式碼應用於我們的資料。在簡要介紹之後,我們將編寫新的程式碼來生成訓練和測試資料集,並將其用於新的監督學習模型。
神經網路簡介
我們已經瞭解了無監督學習模型的工作原理,現在可以將注意力轉向監督學習模型。我們要介紹的監督學習模型是人工神經網路(ANN)模型,或簡稱神經網路。神經網路的靈感來自人類大腦的工作原理。人類大腦由神經元組成,每個神經元接收刺激並決定是否應該被「啟動」。被啟動的神經元會向其他連線的神經元傳送電訊號。如果有一個由相互連線的神經元組成的大型網路,它們可以透過調整連線方式和對刺激的敏感度來學習對不同輸入做出反應。
現代神經網路模型使用相同的指導原則,但更側重於使用資料解決經驗問題,而不是試圖模擬人類大腦。神經網路模型的關鍵元件之一是神經元(有時稱為節點;如圖9-9所示)。一個節點由一個或多個輸入(x_i)、它們的權重(w_i)、輸入函式和啟動函式組成。
節點結構
// 簡化的節點結構範例
struct Node {
inputs: Vec<f64>,
weights: Vec<f64>,
input_function: fn(Vec<f64>, Vec<f64>) -> f64,
activation_function: fn(f64) -> f64,
}
內容解密:
inputs:代表輸入值(x_i)。weights:代表對應輸入值的權重(w_i)。input_function:計算輸入值的加權總和。activation_function:決定節點是否被啟動。
神經網路的運作
節點需要被組合成一個網路。一個簡單的例子如圖9-10所示,包含兩個輸入節點、中間層的兩個節點和一個輸出節點。對於狗或貓的例子,您可以將身高和長度值傳送到兩個輸入節點,輸出節點應該給出一個訊號,指示輸入資料是否屬於狗或貓。每個節點將根據從前一階段獲得的輸入結合權重來決定是否應該被啟動。
簡單的神經網路範例
// 簡化的神經網路範例
struct NeuralNetwork {
layers: Vec<Vec<Node>>,
}
impl NeuralNetwork {
fn new(layers: Vec<Vec<Node>>) -> Self {
NeuralNetwork { layers }
}
fn forward(&self, inputs: Vec<f64>) -> Vec<f64> {
// 前向傳播邏輯
// ...
}
}
內容解密:
NeuralNetwork結構包含多層節點。forward方法實作前向傳播邏輯,將輸入值透過網路傳播。
訓練神經網路
在開始時,您可以隨機設定權重,但這不會比隨便猜測更好。您需要根據訓練資料調整節點中的權重。可以將神經網路的輸出與您掌握的真實答案進行比較;如果輸出相差甚遠,則意味著您需要調整權重,以便下次做出更好的預測。透過損失函式來評估模型目前的表現(或差勁程度)。Rusty-machine預設使用二元交叉熵損失函式,如果輸出(貓或狗)與真實答案不符,則會給出較高的值,反之亦然。目標是調整權重以最小化損失(即,使損失函式傳回盡可能小的值)。為此,您使用一種稱為梯度下降的演算法。梯度下降將以使損失減少的方向調整權重。
梯度下降範例
// 簡化的梯度下降範例
fn gradient_descent(weights: &mut Vec<f64>, loss: f64, learning_rate: f64) {
for weight in weights.iter_mut() {
*weight -= learning_rate * loss;
}
}
內容解密:
gradient_descent函式根據損失和學習率調整權重。
準備訓練資料和測試資料
要開始這個例子,請建立一個新的專案,切換到該目錄,並新增必要的crate,如下所示:
$ cargo new cat-neural-net
$ cd cat-neural-net
$ cargo add serde --features derive
$ cargo add rand rand_distr ndarray csv toml
$ cargo add clap --features derive
為了保持示例簡單,您將使用與K-means示例相同的輸入:身高和長度。您將為貓和狗各建立2,000個樣本。但此資料與K-means資料之間存在一些差異:
- 您需要為每個樣本提供「答案」或真實標籤。
產生訓練資料範例
use rand::Rng;
use rand_distr::{Distribution, Normal};
// 生成樣本資料
fn generate_samples() -> Vec<(f64, f64, bool)> {
let mut rng = rand::thread_rng();
let mut samples = Vec::new();
for _ in 0..2000 {
let height = Normal::new(10.0, 2.0).unwrap().sample(&mut rng);
let length = Normal::new(15.0, 3.0).unwrap().sample(&mut rng);
samples.push((height, length, true)); // 貓
}
for _ in 0..2000 {
let height = Normal::new(20.0, 4.0).unwrap().sample(&mut rng);
let length = Normal::new(25.0, 5.0).unwrap().sample(&mut rng);
samples.push((height, length, false)); // 狗
}
samples
}
內容解密:
- 使用
rand和rand_distrcrate生成隨機樣本資料。 - 為貓和狗生成不同的身高和長度分佈。
人工智慧與機器學習中的資料生成與神經網路訓練
在人工智慧和機器學習領域,訓練一個神經網路模型需要大量的資料。這些資料通常被分成兩部分:訓練資料和測試資料。訓練資料用於訓練模型,而測試資料則用於評估模型的準確性。
資料生成的必要性
神經網路是一種監督式學習模型,這意味著它需要透過比較自己的預測結果與真實標籤之間的差異來進行學習。因此,我們需要為模型提供帶有真實標籤的資料。
資料分割的關鍵
將資料分成訓練集和測試集是非常重要的。訓練集用於訓練模型,而測試集則用於驗證模型的準確性。一個關鍵點是,模型在訓練階段絕不能接觸到測試集中的資料。否則,它將會因為記住了測試資料而輕易達到100%的準確性,即使演算法本身並沒有故意去記住答案,使用測試資料進行訓練通常也會導致過度擬合。
過度擬合的問題
過度擬合是指模型過度適應特定的訓練資料集,而無法產生足夠通用的模型來處理新的、但不完全相同的資料。這意味著模型在相同的訓練資料集上表現非常好,但在任何它之前沒有見過的資料上表現卻很差。
生成訓練資料的程式碼結構
生成訓練資料的程式碼結構與之前的例子類別似,主要差異在於generate_data()函式的實作。以下是一個範例程式碼,用於生成訓練和測試資料:
use clap::Parser;
use ndarray::Array2;
use rand::distributions::Distribution;
use rand::thread_rng;
use rand_distr::Normal;
use serde::Deserialize;
use serde::Serialize;
use std::fs::read_to_string;
use std::io;
use std::error::Error;
#[derive(Deserialize)]
struct Config {
centroids: [f64; 4],
noise: f64,
samples_per_centroid: usize,
}
#[derive(Debug, Serialize)]
struct Sample {
height: f64,
length: f64,
category_id: usize,
}
fn generate_data(
centroids: &Array2<f64>,
points_per_centroid: usize,
noise: f64
) -> Vec<Sample> {
// 實作細節...
}
#[derive(Parser)]
struct Args {
#[arg(short = 'c', long = "config-file")]
config_file_path: std::path::PathBuf,
}
fn main() -> Result<(), Box<dyn Error>> {
// 實作細節...
}
程式碼解密:
generate_data函式:此函式負責根據提供的中心點、每個中心點的樣本數量和雜訊水平生成模擬資料。它首先檢查輸入的有效性,然後使用常態分佈隨機產生圍繞每個中心點的樣本。Sample結構體:定義了輸出的樣本格式,包括高度、長度和類別ID。這裡使用Serialize特徵使得樣本可以被序列化成CSV格式。Config結構體:用於反序列化組態檔案中的引數,如中心點座標、雜訊水平和每個中心點的樣本數量。main函式:解析命令列引數,讀取組態檔案,生成資料,並將結果輸出到標準輸出。
設定檔範例
以下是一個設定檔的範例,用於生成貓和狗的初始種群資料:
centroids = [
22.5, 40.5,
38.0, 50.0,
]
noise = 1.8
samples_per_centroid = 2000
設定檔解密:
centroids:定義了不同類別的中心點座標,例如貓和狗的平均身高和長度。noise:控制了生成資料時的雜訊水平,用於模擬真實世界中的變異性。samples_per_centroid:指定了每個中心點要生成的樣本數量。