[Golang] 應用程式設計教學:實作向量 (Vector) 和矩陣 (Matrix)

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
【贊助商連結】

    前言

    有時候我們要處理的不是單一的數字,而是多個數字所組成的向量 (vector) 或矩陣 (matrix)。向量是一維的資料,用於許多的數學問題中,像是資料探勘中單一樣本的特徵 (features)。除了向量,我們有時候也會將數字儲存在矩陣中,矩陣是線性代數基本的組件,用來代表多維的資料。

    向量和矩陣是數學上的抽象概念,在電腦程式中會用適當的資料結構來實作。本文介紹 Go 如何實作向量和矩陣。

    實作向量 (Vector)

    不論是從語法或標準函式庫的角度來看,Go 並未支援向量運算。目前並沒有什麼最好的套件,一些科學運算的套件各自實作自己的向量。如果各位讀者需要現成的向量物件,可參考 gonum,這個專案內部使用人們所熟知的 BLAS 函式庫進行向量運算。

    不過,必要時,自己實作向量並不會太困難。以下展示一個簡單的純 Go 語言向量實作,程式碼略長,請讀者稍微耐心看一下。

    package vector
    
    type Vector []float64
    
    func New(args ...float64) *Vector {
    	v := make(Vector, len(args))
    
    	for i, e := range args {
    		v.SetAt(i, e)
    	}
    
    	return &v
    }
    
    func WithSize(size int) *Vector {
    	v := make(Vector, size)
    
    	for i := 0; i < size; i++ {
    		v.SetAt(i, 0.0)
    	}
    
    	return &v
    }
    
    func FromArray(arr []float64) *Vector {
    	v := make(Vector, len(arr))
    
    	for i, e := range arr {
    		v.SetAt(i, e)
    	}
    
    	return &v
    }
    
    // The length of the vector
    func (v *Vector) Len() int {
    	return len(*v)
    }
    
    // Getter
    func (v *Vector) At(i int) float64 {
    	if i < 0 || i >= v.Len() {
    		panic("Index out of range")
    	}
    
    	return (*v)[i]
    }
    
    // Setter
    func (v *Vector) SetAt(i int, data float64) {
    	if i < 0 || i >= v.Len() {
    		panic("Index out of range")
    	}
    
    	(*v)[i] = data
    }
    
    // Vector addition
    func Add(v1 *Vector, v2 *Vector) *Vector {
    	_len := v1.Len()
    
    	if !(_len == v2.Len()) {
    		panic("Unequal vector size")
    	}
    
    	out := WithSize(_len)
    
    	for i := 0; i < _len; i++ {
    		a := v1.At(i)
    		b := v2.At(i)
    
    		out.SetAt(i, a+b)
    	}
    
    	return out
    }

    實作向量的基本資料結構是數列 (array)。我們會把該資料結構包成物件,方便我們日後加入新的方法 (method)。

    在本範例中,我們實作三種不同的建構函式,這是為了方便向量使用者以不同的方式初始化向量。

    在實作向量時,實作向量元素的存取函式是基本的步驟,日後要實作其他功能時,會以這些存取函式為基礎。

    實作向量的目的在於預先寫好一些方法,之後在操作向量時,就不用每次從頭撰寫這些方法。在本範例中,為了節省版面,我們只實作向量加法;讀者如果想要練習,可以參考向量加法的部分,繼續實作其他的向量運算。

    以下是根據上述向量所建立的使用範例:

    package main
    
    import (
    	"log"
    	"vector"
    )
    
    func main() {
    	// Create two vectors.
    	v1 := vector.New(1, 2, 3)
    	v2 := vector.New(2, 3, 4)
    
    	// Vector addition.
    	v := vector.Add(v1, v2)
    
    	if !(v.At(0) == 3.0) {
    		log.Fatal("Wrong value")
    	}
    
    	if !(v.At(1) == 5.0) {
    		log.Fatal("Wrong value")
    	}
    
    	if !(v.At(2) == 7.0) {
    		log.Fatal("Wrong value")
    	}
    }

    為了節省文章篇幅,我們這裡僅實作和使用向量加法,有興趣的讀者可參考我們的程式碼繼續擴充這個向量物件。

    實作矩陣 (Matrix)

    同樣的,Go 本身沒有內建的矩陣運算相關型別,我們前面提到的 gonum 套件也有實作矩陣及其相關的運算,有需要時可以拿來用。有一些比較小型、不知名的社群專案試著實作矩陣運算,基本上都沒有形成社群的共識。

    必要時,重新實作一個矩陣不會太困難。在本範例中,我們展示一個純 Go 語言實作的矩陣,程式碼略長,請讀者耐心讀一下。

    package matrix
    
    type Matrix struct {
    	row int
    	col int
    	mtx []float64
    }
    
    func New(mtx [][]float64) *Matrix {
    	row := len(mtx)
    	col := len(mtx[0])
    
    	for i := 1; i < row; i++ {
    		if len(mtx[i]) != col {
    			panic("Unequal column size")
    		}
    	}
    
    	m := WithSize(row, col)
    
    	for i := 0; i < row; i++ {
    		for j := 0; j < col; j++ {
    			m.SetAt(i, j, mtx[i][j])
    		}
    	}
    
    	return m
    }
    
    func WithSize(row int, col int) *Matrix {
    	if row < 0 {
    		panic("Invalid row size")
    	}
    
    	if col < 0 {
    		panic("Invalid column size")
    	}
    
    	m := new(Matrix)
    
    	m.row = row
    	m.col = col
    	m.mtx = make([]float64, m.row*m.col)
    
    	return m
    }
    
    // The row size of the matrix.
    func (m *Matrix) Row() int {
    	return m.row
    }
    
    // The column size of the matrix.
    func (m *Matrix) Col() int {
    	return m.col
    }
    
    // Getter
    func (m *Matrix) At(r int, c int) float64 {
    	if r < 0 || r >= m.Row() {
    		panic("Invalid row size")
    	}
    
    	if c < 0 || c >= m.Col() {
    		panic("Invalid column size")
    	}
    
    	return m.mtx[r*m.Col()+c]
    }
    
    // Setter
    func (m *Matrix) SetAt(r int, c int, data float64) {
    	if r < 0 || r >= m.Row() {
    		panic("Invalid row size")
    	}
    
    	if c < 0 || c >= m.Col() {
    		panic("Invalid column size")
    	}
    
    	m.mtx[r*m.Col()+c] = data
    }
    
    // Matrix element-wise addition
    func Add(m1 *Matrix, m2 *Matrix) *Matrix {
    	row := m1.Row()
    	col := m1.Col()
    
    	if row != m2.Row() {
    		panic("Unequal row size")
    	}
    
    	if col != m2.Col() {
    		panic("Unequal column size")
    	}
    
    	out := WithSize(row, col)
    
    	for i := 0; i < row; i++ {
    		for j := 0; j < col; j++ {
    			a := m1.At(i, j)
    			b := m2.At(i, j)
    
    			out.SetAt(i, j, a+b)
    		}
    	}
    
    	return out
    }

    在我們這個實作中,我們將資料存在一維的切片中,而非二維的切片的切片,因為前者的運算效率會稍微好一些。在這個前提下,我們必需將索引值 (index value) 由二維轉換為一維。轉換時可以使用 row major 或是 column major 來轉換索引,此處採用 row major。

    矩陣要考慮其大小,所以需要實作取得 row 和 column 的方法。像是六個元素的矩陣,可能是 1x62x33x26x1 等多個不同的大小。

    同樣地,我們可以預先實作一些矩陣運算,日後就可以直接使用,不用每次都重寫。為了避免篇幅過長,我們這裡僅實作矩陣加法;讀者可參考矩陣加法的部分,自行實作其他的矩陣運算。

    我們根據以上的矩陣撰寫簡單的使用範例:

    package main
    
    import (
    	"log"
    	"matrix"
    )
    
    func main() {
    	m1 := matrix.New(
    		[][]float64{
    			[]float64{1.0, 2.0, 3.0},
    			[]float64{4.0, 5.0, 6.0},
    		},
    	)
    
    	m2 := matrix.New(
    		[][]float64{
    			[]float64{2.0, 3.0, 4.0},
    			[]float64{5.0, 6.0, 7.0},
    		},
    	)
    
    	m := matrix.Add(m1, m2)
    
    	if !(m.At(0, 0) == 3.0) {
    		log.Fatal("Wrong value")
    	}
    
    	if !(m.At(0, 1) == 5.0) {
    		log.Fatal("Wrong value")
    	}
    
    	if !(m.At(0, 2) == 7.0) {
    		log.Fatal("Wrong value")
    	}
    
    	if !(m.At(1, 0) == 9.0) {
    		log.Fatal("Wrong value")
    	}
    
    	if !(m.At(1, 1) == 11.0) {
    		log.Fatal("Wrong value")
    	}
    
    	if !(m.At(1, 2) == 13.0) {
    		log.Fatal("Wrong value")
    	}
    }

    平均數 (Mean)、中位數 (Median)、眾數 (Mode)

    有向量型別後,我們就可以進一步計算平均數 (mean)、中位數 (median)、眾數 (mode) 等基本的統計學指標;基本上 R 語言和 Python 的發展模式也是差不多這樣。參考實作如下:

    // Declare Vector as above.
    
    func (v *Vector) Sort() IVector {
    	if v.Len() == 0 {
    		return v
    	}
    
    	arr := make([]float64, 1)
    	arr[0] = v.At(0)
    
    	for i := 1; i < v.Len(); i++ {
    		inserted := false
    
    		for j := 0; j < len(arr); j++ {
    			if v.At(i) < arr[j] {
    				if j == 0 {
    					arr = append([]float64{v.At(i)}, arr...)
    				} else {
    					arr = append(arr[:j], append([]float64{v.At(i)}, arr[j:]...)...)
    				}
    
    				inserted = true
    				break
    			}
    		}
    
    		if !inserted {
    			arr = append(arr, v.At(i))
    		}
    	}
    
    	return FromArray(arr)
    }
    
    func Mean(v IVector) float64 {
    	return Sum(v) / float64(v.Len())
    }
    
    func Median(v IVector) float64 {
    	if v.Len() == 0 {
    		panic("No valid vector data")
    	} else if v.Len() == 1 {
    		return v.At(0)
    	}
    
    	sorted := v.Sort()
    	_len := sorted.Len()
    
    	if _len%2 == 0 {
    		i := _len / 2
    		return (sorted.At(i-1) + sorted.At(i)) / float64(2)
    	} else {
    		i := _len / 2
    		return sorted.At(i)
    	}
    }
    
    func Mode(v IVector) IVector {
    	if v.Len() == 0 {
    		panic("No valid vector data")
    	} else if v.Len() == 1 {
    		return v
    	}
    
    	m := make(map[string]int)
    
    	max := -1
    	for i := 0; i < v.Len(); i++ {
    		f := strconv.FormatFloat(v.At(i), 'E', -1, 64)
    		_, ok := m[f]
    		if !ok {
    			m[f] = 0
    		} else {
    			m[f]++
    		}
    
    		if m[f] > max {
    			max = m[f]
    		}
    	}
    
    	arr := make([]float64, 0)
    	for k, _ := range m {
    		if m[k] >= max {
    			f, _ := strconv.ParseFloat(k, 64)
    			arr = append(arr, f)
    		}
    	}
    
    	return FromArray(arr).Sort()
    }

    在建立向量和矩陣型別後,我們就可以進一步發展統計和科學運算的套件。不過,這已經超出這篇文章的範圍,有興趣的讀者可參考相關的數理公式,自行實作看看。

    結語

    在 Go 語言中,我們可以用 gonum 等套件來處理向量或矩陣運算,不過,目前 Go 社群尚未對此議題擬聚共識。目前 Go 社群缺乏像 Python 的 NumPy 這種套件,每個團隊必需重新實作一些基礎的功能,對於科算運算套件來說相對不利。

    在 Go 使用向量和矩陣運算很難像 R 或 MATLAB 那麼方便,因為後者將向量和矩陣的資料結構直接內建在語言中;此外,Go 也缺乏良好的互動式環境 (REPL),不利於交互式使用。在探索式分析 (explorative analyses) 時,我們通常會用 R、Python 或 MATLAB 等工具,而將 Go 語言保留在在批次處理或是建立應用程式時使用。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【支持站長】
    Buy me a coffeeBuy me a coffee
    【贊助商連結】
    【贊助商連結】
    【贊助商連結】
    【分類瀏覽】