RPPAL 函式庫簡化了樹莓派 GPIO 的操作,它巧妙地利用 /dev/gpiomem 虛擬裝置,避免了直接操作 /dev/mem 需要的 root 許可權與潛在安全風險。透過記憶體對映,RPPAL 能夠直接讀寫 GPIO 相關的記憶體位址,有效控制針腳狀態。文章提供的程式碼片段展示了 RPPAL 如何透過 libc::mmap 對映記憶體,並利用位元運算精確控制 GPIO 輸出。接著,文章將焦點轉向機器學習,比較了監督式學習與非監督式學習的差異,並以 K-means 演算法為例,說明如何使用 rusty-machine 套件進行貓品種聚類別。文章也提供程式碼示範如何生成用於訓練 K-means 模型的貓身體測量資料,為讀者提供實作參考。

物理計算的進階探索

在前面的章節中,我們探討了使用Rust進行物理計算的基本概念,包括如何使用RPPAL函式庫控制樹莓派上的GPIO針腳。現在,讓我們進一步探索物理計算的世界,瞭解如何擴充套件我們的專案,並深入研究嵌入式系統的開發。

RPPAL的內部實作

RPPAL函式庫提供了一個方便的方式來存取GPIO針腳,但它的內部實作是怎樣的呢?RPPAL使用了一個稱為/dev/gpiomem的虛擬裝置來克服許可權問題。在此之前,開發者需要使用/dev/mem來存取GPIO相關的記憶體位址,但這需要root許可權,並且存在安全風險。/dev/gpiomem的出現解決了這個問題,它只暴露了GPIO相關的記憶體部分,無需特殊許可權。

在RPPAL的原始碼中,我們可以看到它首先嘗試使用/dev/gpiomem,如果發生錯誤,則會退回到/dev/mem,但這需要root許可權。下面是RPPAL中操作GPIO的相關程式碼:

const PATH_DEV_GPIOMEM: &str = "/dev/gpiomem";
const GPFSEL0: usize = 0x00;
const GPSET0: usize = 0x1c / std::mem::size_of::<u32>();
const GPCLR0: usize = 0x28 / std::mem::size_of::<u32>();
const GPLEV0: usize = 0x34 / std::mem::size_of::<u32>();

pub struct GpioMem {}

impl GpioMem {
    // ...
    fn map_devgpiomem() -> Result<*mut u32> {
        // ...
        // 將/dev/gpiomem對映到虛擬記憶體
        let gpiomem_ptr = unsafe {
            libc::mmap(
                ptr::null_mut(),
                GPIO_MEM_SIZE,
                PROT_READ | PROT_WRITE,
                MAP_SHARED,
                gpiomem_file.as_raw_fd(),
                0,
            )
        };
        Ok(gpiomem_ptr as *mut u32)
    }

    #[inline(always)]
    fn write(&self, offset: usize, value: u32) {
        unsafe {
            ptr::write_volatile(self.mem_ptr.add(offset), value);
        }
    }

    #[inline(always)]
    pub(crate) fn set_high(&self, pin: u8) {
        let offset = GPSET0 + pin as usize / 32;
        let shift = pin % 32;
        self.write(offset, 1 << shift);
    }

    #[inline(always)]
    pub(crate) fn set_low(&self, pin: u8) {
        let offset = GPCLR0 + pin as usize / 32;
        let shift = pin % 32;
        self.write(offset, 1 << shift);
    }

    pub(crate) fn set_mode(&self, pin: u8, mode: Mode) {
        let offset = GPFSEL0 + pin as usize / 10;
        let shift = (pin % 10) * 3;
        // ...
        let reg_value = self.read(offset);
        self.write(
            offset,
            (reg_value & !(0b111 << shift)) |
            ((mode as u32) << shift),
        );
    }
}

內容解密:

  • map_devgpiomem函式使用libc::mmap/dev/gpiomem對映到虛擬記憶體,允許直接存取GPIO相關的記憶體位址。
  • write函式用於向指定的記憶體位址寫入值,實作對GPIO針腳的控制。
  • set_highset_low函式分別用於將指定的GPIO針腳設為高電平或低電平。
  • set_mode函式用於設定GPIO針腳的工作模式。

探索更多可能性

本章節僅僅觸及了物理計算和嵌入式系統開發的表面。Rust的嵌入式生態系統正在不斷發展,有很多方向可以探索。以下是一些建議:

  • 擴充套件樹莓派的功能:除了LED和按鈕之外,還有很多硬體可以連線到樹莓派,如蜂鳴器、光線感測器、聲音感測器、方向感測器、攝影機、濕度及溫度感測器、紅外線感測器、超音波感測器、觸控螢幕和伺服馬達等。
  • 使用HATs(Hardware Attached on Top):HATs是專為樹莓派設計的擴充套件板,它們包含了多種硬體元件,並且可以直接安裝在樹莓派上,簡化了硬體連線的複雜度。例如,Sense HAT提供了一個抽象層,讓開發者可以輕鬆地存取其上的各種感測器。
  • 探索其他開發板和平台:Rust支援多種不同的電腦架構,因此有很多開發板可供選擇。例如,STM32F3DISCOVERY開發板是一個不錯的選擇,Rust Embedded Working Group為其提供了詳細的教學。
  • 深入研究裸機程式設計:裸機程式設計意味著Rust程式直接在硬體上執行,無需作業系統。這需要使用#![no_std]屬性,並依賴於libcore。Rust Embedded Book提供了相關的教學。

未來方向

物理計算和嵌入式系統開發是一個廣闊而有趣的領域,Rust提供了一個安全且高效的方式來進行開發。無論是擴充套件樹莓派的功能,還是探索其他開發板和平台,亦或是深入研究裸機程式設計,都有很多令人興奮的機會等待著你。Are We Embedded Yet?(https://afonso360.github.io/rust-embedded/)是一個很好的資源,可以幫助你跟蹤Rust嵌入式生態系統的最新進展。

人工智慧與機器學習

人工智慧(Artificial Intelligence, AI)與機器學習(Machine Learning, ML)一直是新聞和科幻小說中的熱門話題。近年來,由於深度學習的技術突破以及更多導向消費者的應用程式的出現,這些技術獲得了更多的媒體關注。「機器學習」和「人工智慧」這兩個術語有時會被互動使用,但它們之間存在著微妙的差異。人工智慧關注的是「智慧」。一個AI系統試圖表現得好像它擁有人類的智慧一樣,無論其底層的方法或演算法是什麼。但在機器學習中,重點是「學習」,即系統試圖從資料中學習,而不需要人類明確地程式設計知識。

機器學習的定義

舉例來說,早期AI的成功案例之一是專家系統。在專家系統中,特定領域的知識被寫成規則並直接程式設計到程式碼中,因此係統可以像領域專家一樣回答問題或執行任務。這種系統可能表現出某種程度的人類智慧,但底層並不是真正從資料中「學習」。因此,這種系統可以被稱為AI系統,但不是機器學習系統。

研究人員嘗試了許多不同的策略來構建AI系統,不一定是機器學習。但是,機器學習之所以流行是由於一些技術進步。首先,現代CPU和GPU的計算能力由於硬體技術的創新而呈指數級增長。這意味著機器學習模型終於可以在合理的時間內進行訓練。網際網路的興起也導致了越來越多的資料可以以非常低的成本被收集,因此你終於有了足夠的資料來支援需要大量資料的機器學習演算法,如深度神經網路。所有這些因素都促成了機器學習在近十年的蓬勃發展。

監督式學習與非監督式學習

機器學習有兩個主要分支:監督式學習(Supervised Learning)和非監督式學習(Unsupervised Learning)。在監督式學習環境中,你給演算法一個完全標記的訓練資料集。例如,如果你試圖區分貓和狗的圖片,你需要準備大量的照片,並標記為「貓」或「狗」。因為演算法可以根據標籤(或有時稱為「真實值」)檢查其預測結果,所以演算法可以從錯誤中學習並改進其預測。

但是,完全標記的資料集並不總是可用的。除非有一種自動化的方式能夠以100%的準確度收集標籤,否則你必須手動標記它們。這需要大量的時間和金錢。即使資料可以自動收集,資料的品質也可能因為噪聲而不理想。因此,當無法獲得高品質的完全標記資料集時,你只能依靠非監督式學習演算法來完成任務。非監督式演算法接受沒有標籤的訓練資料集,並嘗試從資料本身中學習模式。例如,如果你想區分不同的花卉物種,你可以讓演算法根據花的顏色、形狀、葉子形狀等進行分組。但是,在沒有真實值標籤可以檢查的情況下,一個演算法可能會關注顏色,並將白玫瑰和白百合歸為同一類別。另一個演算法可能會關注形狀,並將所有顏色的玫瑰歸為一組。因此,非監督式學習通常表現較差或不如監督式模型那樣可預測,但當標記訓練資料難以獲得時,它仍然非常有用。

例項解析

本文使用兩個例子來說明監督式學習和非監督式學習。首先介紹非監督式學習。非監督式學習的程式碼比監督式學習更簡單,因此你可以先了解機器學習程式的基本概念,而不會被細節所淹沒。你將建立一個能夠區分不同貓品種的模型。該模型收集了三個貓品種的身高和體長資料:波斯貓、英國短毛貓和拉格doll貓(見圖6-1至6-3)。由於這三個品種的身高和體長略有不同,你可以在這兩個特徵上執行K-means聚類別模型(在「使用K-means聚類別貓品種」一節中有詳細解釋)。一旦模型訓練完成,就可以用來自動將貓分成不同的品種。因為K-means只能看到資料點之間的相似性,所以它可以將貓分成不同的組,但無法準確地判斷哪一組是哪個貓品種。

圖示說明

此圖示展示了三種不同的貓品種,包括波斯貓、英國短毛貓和拉格doll貓,它們在身高和體長上存在差異。

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Rust 物理計算與機器學習探索

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

人工智慧與機器學習:以 Rust 程式語言實作

監督式學習範例:區分貓與狗

在監督式學習的範例中,我們擁有大量的貓和狗的身體測量資料,並且有標籤指出每個測量資料是來自貓還是狗。利用這些資料集,我們將使用神經網路模型來學習如何區分貓和狗。在機器學習中,這類別任務被稱為分類別問題。當模型訓練完成後,它可以預測給定的身體測量資料屬於貓還是狗,即使這些資料不在訓練集中。為了簡化起見,我們只使用身高和身體長度作為輸入。

準備訓練資料

這些機器學習模型並非存在於真空之中。除了訓練模型和使用模型之外,還涉及許多工,如資料準備、清理和視覺化。我們還將學習如何使用 Rust 生成人工訓練資料,將資料寫入和讀取為 CSV(逗號分隔值)檔案,並視覺化資料。演示程式由幾個鬆散相關的二進位制可執行檔案組成。它們不會直接通訊,而是透過傳遞 CSV 檔案來最小化步驟之間的依賴關係。

介紹 rusty-machine 套件

任何程式語言中的機器學習生態系統都依賴於堅實的基礎。構建機器學習函式庫不僅涉及機器學習演算法本身,還涉及許多基礎操作,如數值計算、線性代數、統計和資料操作。

本章使用 rusty-machine 套件。rusty-machine 套件包含許多用 Rust 實作的傳統機器學習演算法。雖然深度學習是目前機器學習中最熱門的話題,但尚未有成熟的根據 Rust 的函式庫。大多數 Rust 中的深度學習函式庫都是對其他語言函式庫的繫結,因此 API 設計並不是非常 Rust 風格。深度學習模型也由於涉及更先進的數學理論而更難被直觀理解,這可能會分散對程式碼架構和 Rust API 的注意力。

rusty-machine 套件包含的機器學習演算法

  • 線性迴歸
  • 邏輯迴歸
  • 廣義線性模型
  • K-means 聚類別
  • 神經網路
  • 高斯過程迴歸
  • 支援向量機
  • 高斯混合模型
  • 樸素貝葉斯分類別器
  • DBSCAN
  • K-近鄰分類別器
  • 主成分分析

它使用 rulinalg 套件進行線性代數運算。rulinalg 套件的一部分被重新匯出在 rusty_machine::linalg 名稱空間中,因此無需手動匯入 rulinalg。它還包含有用的資料轉換工具,用於資料預處理,稍後將用於規範化。

使用 K-Means 演算法對貓品種進行聚類別

K-Means 演算法簡介

你不需要是貓專家就能識別不同的貓品種。波斯貓與英國短毛貓在許多方面看起來不同:它們的毛髮長度不同,臉型不同,平均大小也不同。對事物進行分類別是人類的特徵,有助於我們理解這個世界。但是機器沒有這種直覺,因此我們需要將數學的力量程式設計到它們中。這類別問題被稱為聚類別問題,而解決這個問題的一種流行演算法是 K-means。

由於這是一本導向一般 Rust 愛好者的書,而不是數學家的書,因此我將用通俗的語言來解釋這個概念。你可以透過在網上搜尋“K-means”輕鬆找到正式的數學定義。

K-Means 演算法的步驟

  1. 隨機分配 k 個點作為“中心點”。中心點是每個聚類別的中心。
  2. 分配:對於所有其他點,將它們分配給最近的中心點所在的組。
  3. 更新中心點:對於每個組,找到該組中所有點的中心點(即平均值),並將該中心點用作新的中心點。
  4. 重複步驟 2 和 3,直到中心點不再移動。

你可以想象,在每次更新過程中,中心點將朝向點“雲”的中心移動,而在下一次分配中,一些點可能會因為中心點位置的變化而被分配到新的中心點。你繼續這個過程,直到中心點不再移動;這時,你可以說模型已經收斂。你可以在圖 6-4 中看到一個圖形示例。

K-Means++ 演算法

在實踐中,初始中心點的位置對最終結果影響很大。如果初始中心點分配得不好,演算法可能會收斂到一個不是理想的結果。它也可能需要更長的時間才能收斂。你可以使用一種稱為 K-means++ 的演算法來更好地初始化初始中心點。其背後的直覺是,你希望將初始中心點盡可能地分散開。具體步驟如下:

  1. 從所有點中隨機選擇第一個中心點。
  2. 對於每個點 x,計算到其最近的現有中心點的距離 D(x)。
  3. 要找到下一個中心點,請選擇一個與 D(x)^2 成比例的機率的點。這意味著,如果一個點 x 距離任何現有中心點越遠,其 D(x) 越大,並且它被選為新中心點的機率越高。
  4. 重複步驟 2 和 3,直到所有中心點都被選中。

透過使用 K-means++ 演算法,初始中心點盡可能地分散開。這通常會導致更好的結果。這是 rusty-machine 預設使用的初始化方法。

生成訓練資料

為了準備訓練資料,你需要收集許多貓的身體測量資料。由於這很耗時,而且你可能不想被脾氣暴躁的貓抓傷或咬傷,本示例使用平均值生成一個假資料集。你可以生成一些以平均值為中心的正態分佈的人工貓身體測量資料。身體測量資料包括:

  • 身高:從地面到貓肩部的高度
  • 長度:從貓頭到其臀部的長度,不包括尾巴
// 以下是一個簡單的範例程式碼,用於生成人工貓身體測量資料
use rand::distributions::{Distribution, Normal};

fn generate_cat_data(num_samples: usize) -> Vec<(f64, f64)> {
    let height_dist = Normal::new(25.0, 2.0); // 平均身高 25cm,標準差 2cm
    let length_dist = Normal::new(40.0, 3.0); // 平均長度 40cm,標準差 3cm
    let mut data = Vec::new();

    for _ in 0..num_samples {
        let height = height_dist.sample(&mut rand::thread_rng());
        let length = length_dist.sample(&mut rand::thread_rng());
        data.push((height, length));
    }

    data
}

fn main() {
    let cat_data = generate_cat_data(100);
    for (height, length) in cat_data {
        println!("Height: {}, Length: {}", height, length);
    }
}

內容解密:

  1. 匯入必要的函式庫:使用 rand 函式庫來生成隨機數,以及 rand::distributions 中的 Normal 分佈來生成正態分佈的隨機數。
  2. 定義生成貓資料的函式generate_cat_data 函式接受一個引數 num_samples,表示要生成的樣本數量。它使用 Normal 分佈來生成身高和長度的隨機數。
  3. Normal 分佈引數:身高的平均值設為 25.0,標準差設為 2.0;長度的平均值設為 40.0,標準差設為 3.0。這些引數可以根據實際情況進行調整。
  4. 生成資料:在 generate_cat_data 函式中,使用迴圈生成指定數量的樣本。每個樣本包含一個身高和一個長度,這兩個值都是根據指定的正態分佈生成的。
  5. main 函式:在 main 函式中,呼叫 generate_cat_data 函式生成 100 個樣本,並列印出每個樣本的身高和長度。

這個範例展示瞭如何使用 Rust 生成人工貓身體測量資料,為後續的機器學習任務做準備。