前言
有時候我們要處理的不是單一的數字,而是多個數字所組成的向量 (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 的方法。像是六個元素的矩陣,可能是 1x6
、2x3
、3x2
、6x1
等多個不同的大小。
同樣地,我們可以預先實作一些矩陣運算,日後就可以直接使用,不用每次都重寫。為了避免篇幅過長,我們這裡僅實作矩陣加法;讀者可參考矩陣加法的部分,自行實作其他的矩陣運算。
我們根據以上的矩陣撰寫簡單的使用範例:
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 語言保留在在批次處理或是建立應用程式時使用。