python vector 初始化_从零开始搭建机器学习算法框架(python)--计算框架
介紹
今天開始一個新的系列,這個系列的目標是用python在不使用任何第三方庫的情況下去實現各類機器學習或者深度學習的算法。之所以會有這種想法是因為每當我想提高編程技巧的時候,我總希望能夠做一些簡單又有趣的小項目練手。我一直對機器學習算法頗感興趣,所以我想為什么不用python從零開始搭建一套迷你機器學習庫呢。于是我嘗試這么做了,這個系列就是記錄我實現這一想法的過程。另外,由于這個項目很少用到第三方庫,而且實現上盡可能抱著語法簡單,因此也較為容易轉換成其他語言。
說起來,在沒有開始這個項目之前,有些東西,比如Numpy里的array賦值、取值、轉置這些,用起來跟呼吸一樣自然,并造成一種...就像被問1+1為什么等于2的感覺:它就是應該等于2沒有為什么。但真正深入實現的時候,發現又不是這么一回事...
本篇是系列的第一篇,主要是模仿numpy的部分功能,搭建一個矩陣計算框架。當然,這個實現不會像商業庫那樣擁有強大的功能以及穩定性,因而會有些不那么robust。但對于抱著學習的目的來說,忽略一些復雜情況可以更容易理解本質。我打打算是每一期的代碼都是最簡實現,夠用就行,只有后面實現算法時需要用到新功能時才會新增功能。那么下面就開始這個矩陣計算框架的第一步,Vector類。
1.Vector類
矩陣運算首先得要有矩陣,numpy里面矩陣的展現形式是ndarray這個類,pytorch或者tensorflow都叫Tensor。我這里起個名字叫Vector吧,用于存儲矩陣的數據結構。
1.1 初始化函數
關于Vector類,有兩個必不可少的類成員屬性,一是用于存儲數值的變量array,二是用于表示矩陣形狀的變量shape。
總之,我們希望如果對Vector進行索引的話,得到的東西還是個vector。所以最簡單的想法就是,array里裝的是低一維的Vector。這樣索引的時候直接可以對array索引直接取到響應的Vector了。這樣,array在初始化的時候需要做一些額外的操作,直接看代碼好了:
class Vector:def __init__(self, data, shape=None, requires_grad=False, grad=None, _creator=None, name='Unknown'):self.name = nameif not shape:self.shape = _inference_shape(data)else:self.shape = deepcopy(shape)if self.ndim > 1:self.array = [v if isinstance(v, Vector) else Vector(v, shape=self.shape[1:]) for v in data]else:self.array = [cast(v) for v in data]self.grad = gradself.requires_grad = requires_gradself._creator = _creator上述代碼看到,初始化Vector最主要是兩個參數,一個是data,即存了什么樣的數據。另一個是shape,表明了數據按照什么樣的格式存儲的(其他參數是自動求導所需要的,后一期再做介紹)。需要注意的是,如果data本身具備多維的結構,比如嵌套的list,那么shape可以為None,此時,shape可以從data的嵌套結構中推測出來,即_inference_shape()這個函數。該函數的作用是給定一個多維list,并返回這個list的shape,具體實現放在了后面。
接下來,如果data是一個高維的結構,那么array里存的是低一維度的Vector,因此可以通過遞歸構造Vector。當data是一維的時候,意味著array里存的是數值,直接把data存入array就好了。這里需要注意的是我為python中的int以及float分別新建了一個類Int以及Float。這么做的原因說主要是想把value的傳遞方式從按值傳遞轉變為引用傳遞。考慮到當vector進行轉置操作時會新生成一個vector,但是我們希望對轉置后的vector進行修改時,原始vector的值也跟著修改,畢竟他倆是同一個東西,只不過shape變了而已。如果使用原始數據類型的話,賦值時會按值傳遞的,也就無法實現這一效果。
題外話:關于變量array存儲數據的形式,我在這里還踩了兩個坑。一開始我很天真,以為多維矩陣就是list的嵌套,即Vector的array就是諸如:[[1,2,3],[4,5,6]]。但是隨著進度繼續,我發現單純list的嵌套會有很多麻煩的地方。其中第一不和諧的點是,如果array為list的嵌套,對Vector的某個維度進行索引的時候,取出來是個list,而不是一個Vector。盡管在復寫__getitem__的時候,可以構造一個新的Vector實例,但每次索引都要初始化一個Vector,這會影響索引時的速度...當然,list作為線性表(當然python里沒有嚴格的線性表,list是線性表和鏈表的結合),索引速度是很快的,于是我打算讓Vector類在索引時盡可能維持線性表的特性,于是產生了我第二個想法。第二個想法有點極端:我讓array的存儲結構就是一個一維的list。對vector取值時,可以根據shape把index進行映射到array相應的index上,來達到索引指定位置的目的,思路類似下圖(圖一,圖一中展示了一個三維矩陣,當索引(0,2,0)這個位置時,可以通過簡單的換算把(0,2,0)轉換成list上第5個位置)。這種實現有很多方便的地方,比如做element-wise的運算時。但是,在寫轉置操作時,我覺得這種方案非常難寫,于是放棄了這個方案。圖一1.2 數據索引取值
python中如果想實現對某個類的實例進行索引取值,就是重寫__getitem__方法。類似numpy,索引支持切片,即vector[1:3],以及多維度索引,vector[1,2,3]。
def __getitem__(self, index):def recursive_getitem(vector, index_):if len(index_) > 0:res_array_ = []if isinstance(index_[0], slice):for elem in vector.array[index_[0]]:res_array_.append(recursive_getitem(elem, index_[1:]))else:res_array_ = recursive_getitem(vector.array[index_[0]], index_[1:])else:return vectorreturn res_array_if isinstance(index, int):return self.array[index]elif isinstance(index, slice):start = index.start if index.start else 0end = index.stop if index.stop else self.shape[0]step = index.step if index.step else 1res_array = [self[i].array if isinstance(self[i], Vector) else self[i] for i in range(start, end, step)]elif isinstance(index, tuple) or isinstance(index, list):if len(index) < self.ndim:index = list(index)index.extend([slice(None, None, None)] * (self.ndim - len(index)))res_array = recursive_getitem(self, index)else:raise Exception()if isinstance(res_array, list):return Vector(res_array)else:return res_array__getitem__有一個入參index,表示索引的位置。由于既要支持普通索引,又要支持切片操作同時還要支持高維索引,所以index的類型有三種情況,第一個是最普通的,傳入一個int型數值,第二種傳進來的是slice。前兩種都是在單一維度上的索引。第三種情況傳進來的是一個元祖(或list),表示在多個維度上的索引。最后一個情況處理起來稍微麻煩些,不過思路卻很簡單:由于需要在多個維度上索引,很容易想到按照順序每次只處理一個維度,這樣高維索引問題可以轉化為多次一維的索引。用遞歸很容易解決,只是需要留意下高維索引和切片索引結合的情況,即vector[1,:,3]。
1.3 索引賦值
關于賦值,我犯過一個錯誤,就是想當然的認為,既然有了索引,那么賦值不就很簡單嗎,__setitem__里直接self[key] = value就完事了。但實際上,=操作就是__setitem__,如果__setitem__寫上self[key] = value,那么就等于是循環調用一個沒有任何意義的__setitem__,結果就是死循環。不過,__getitem__寫好了的話,__setitem__也不算特別麻煩:
def __setitem__(self, key, value):t = self[key]if isinstance(t, BaseType):v = cast(value)t.val = v.valelif isinstance(t, Vector):index = [0 for _ in range(t.ndim)]while 1:for axis in range(t.ndim - 1):if index[axis] == t.shape[axis]:index[axis] = 0index[axis + 1] += 1if index[-1] >= t.shape[-1]:breakt[index].val = cast(value[index]).valindex[0] += 1與取值類似,key(對應于__getitem__里的index,原諒我命名沒有統一...)有三種情況(上面代碼漏寫了一種情況)。由于索引取值已經在__getitem__里實現了,剩下的只是賦值而已。這里也只是提醒一點,如果value是python內置的數據類型需要轉化成自定義的Int或Float。
1.4 轉置與交換維度
接下來是一個比較重要的功能,轉置。說實話,可能是我比較反應比較遲鈍,轉置這個操作卡了我很長時間。因為我始終覺得轉置其實就是幾個軸交換順序了,所以我一直想的是如何給Vector加上軸的概念(類似于圖一的結構)這樣轉置會變得異常簡單。但真正做下去以后,總覺得加軸反而會把Vector的結構弄復雜了,于是放棄了這個思路。我現在的實現方式如下:
def transpose(self, axises=None):new_shape = [self.shape[axises[i]] for i in range(self.ndim)]new_vec = zeros(new_shape)curr_index = [0 for _ in range(self.ndim)]target_idx = [0 for _ in range(new_vec.ndim)]while 1:for axis in range(self.ndim - 1):if curr_index[axis] == self.shape[axis]:curr_index[axis] = 0curr_index[axis + 1] += 1if curr_index[-1] >= self.shape[-1]:breakfor i, j in enumerate(axises):target_idx[i] = curr_index[j]new_vec[target_idx] = self[curr_index]curr_index[0] += 1return new_vectranspose接收一個參數axises,表示維度的交換順序:原始的維度為[0,1,2,3],如果打算交換第0維與第2維的位置,則axises為[2,1,0,3]。我這里的實現思路可能不夠高效,但好在還算簡單直白,首先用轉置后的shape初始化一個新的vector,然后遍歷原始vector,把數值一個個塞到新的vector里去。遍歷的過程比較原始,就是通過構造多維索引curr_index,從最低位開始遍歷,每次循環在最低位+1,不過需要注意“進位”。另外target_idx 是轉置后對curr_index的映射。
另外,numpy里還有個swapaxes函數,功能類似transpose,但是只接受兩個參數,即只能交換兩個維度。我這里用transpose來輔助實現的。
def swapaxes(self, axis1=None, axis2=None):axises = [i for i in range(self.ndim)]axises[axis1], axises[axis2] = axises[axis2], axises[axis1]return self.transpose(axises)1.5 To String
還有一個重要的功能是,我希望寫好的Vector類能夠順利的被print出來。在python中,如果想要把一個類print出來,可以覆寫__str__方法。聽起來這個功能似乎很簡單,實際上如果想要輸出的工整一些,還是有些麻煩的:
def __str__(self):max_l = len(str(self.max_num))def recursive_print_vector(vector, index=0):if isinstance(vector, Vector):res = []for i, elem in enumerate(vector.array):s = recursive_print_vector(elem, i)if isinstance(vector.array[0], Vector):if i == len(vector.array) - 1:res.append(str(s))else:res.append('%sn' % s)else:offset = max_l - len(s)prefix = ' ' * offsetres.append('%s%s' % (prefix, s))blank = ' ' * (self.ndim - vector.ndim - 1) if index else ''return blank + '[' + ' '.join(res) + ']'else:return str(vector)return recursive_print_vector(self)另外上述函數需要計算Vector的最大值:
@propertydef min_num(self):min_num = float('inf')index = [0 for _ in range(self.ndim)]while 1:for axis in range(self.ndim - 1):if index[axis] == self.shape[axis]:index[axis] = 0index[axis + 1] += 1if index[-1] >= self.shape[-1]:breakval = self[index].valif min_num < val:min_num = valindex[0] += 1return min_num@propertydef max_num(self):max_num = float('-inf')index = [0 for _ in range(self.ndim)]while 1:for axis in range(self.ndim - 1):if index[axis] == self.shape[axis]:index[axis] = 0index[axis + 1] += 1if index[-1] >= self.shape[-1]:breakval = self[index].valif max_num > val:max_num = valindex[0] += 1return max_num1.6 增加維度
unsqueeze的功能是給vector增加一個維度,這個比較簡單,核心就是對vector進行遍歷。另外看代碼的話,似乎很多函數都用到了對vector進行循環遍歷的功能,看來這塊代碼可以封裝起來...
def unsqueeze(self, dim):if dim == -1:dim = self.ndimnew_shape = deepcopy(self.shape)new_shape.insert(dim, 1)new_vec = zeros(new_shape)index = [0 for _ in range(new_vec.ndim)]while 1:for axis in range(new_vec.ndim - 1):if index[axis] == new_vec.shape[axis]:index[axis] = 0index[axis + 1] += 1if index[-1] >= new_vec.shape[-1]:breakcurr_index = deepcopy(index)del curr_index[dim]new_vec[index] = self[curr_index]index[0] += 1return new_vec1.7 減少維度
有unsqueeze就有squeeze。
def squeeze(self, dim):if self.shape[dim] == 1:if dim == -1:dim = self.ndim - 1new_shape = deepcopy(self.shape)del new_shape[dim]new_vec = zeros(new_shape)index = [0 for _ in range(new_vec.ndim)]while 1:for axis in range(new_vec.ndim - 1):if index[axis] == new_vec.shape[axis]:index[axis] = 0index[axis + 1] += 1if index[-1] >= new_vec.shape[-1]:breakcurr_index = deepcopy(index)curr_index.insert(dim, 0)new_vec[index] = self[curr_index]index[0] += 1return new_vecelse:raise ValueError2.若干工具函數
_inference_shape:功能是推斷嵌套list的shape。
def _inference_shape(data):shape = []while isinstance(data, list):shape.append(len(data))data = data[0]return shapecreate_array_by_shape:與_inference_shape功能相反,是通過shape構造嵌套的list。
def create_array_by_shape(shape, val):if isinstance(shape, int):return valelse:new_shape = shape[1:] if len(shape) > 1 else shape[0]return [create_array_by_shape(new_shape, val) for _ in range(shape[0])]cast:用于轉換類型的函數
def cast(v):if isinstance(v, int):return Int(v)elif isinstance(v, float):return Float(v)elif isinstance(v, BaseType):return velse:raise ValueErrorzeros和ones:構造一個0或1組成的Vector實例。
def zeros(shape):array = create_array_by_shape(shape, 0)return Vector(array, shape=shape)def ones(shape):array = create_array_by_shape(shape, 1)return Vector(array, shape=shape)結語
至此,一個簡單Vector類別就構造好了,它現在可以索引,可以賦值,以及能夠進行轉置操作。作為一個矩陣計算框架,功能似乎還不夠多。更多的功能會在后面需要的時候一點點補充。在下期我打算實現自動求導,以及大部分運算操作。
https://github.com/iron-fe/machine_learning_toys.git?github.com總結
以上是生活随笔為你收集整理的python vector 初始化_从零开始搭建机器学习算法框架(python)--计算框架的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android支付接入(四):联通VAC
- 下一篇: 漏洞编号 cve can bugtraq