開源技術教學文件網 實作向量 (Vector) 和矩陣 (Matrix)

最後修改日期為 MAY 28, 2019

前言

有時候我們要處理的不是單一的數字,而是多個數字所組成的向量 (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 Yahoo
追蹤本站
Facebook Facebook Twitter