上一個博客中講解了用python實現一個簡單的兩層神經網絡,我們是把所有的網絡層都直接寫在了類中。但是作為一個神經網絡框架,網絡的結構應該是可以由使用者自定義的,這樣一來也就不用為每個網絡結構都重寫所有代碼,我們把每一層模塊化,在神經網絡的類中定義結構時使用這些模塊化的層堆疊形成一個完整的神經網絡。每一種層,分別實現forward和password兩個函數,用來正向計算和反向傳播。
這里我們實現的網絡層和功能有:全連層、激活層、計算loss、自動訓練
1、全連層
全連層的正向計算很簡單,f(W,x)=xW+b。
反向傳播分別求x,W,b的梯度,dx=f(W,x)·W.T,dW=x·f(W,x),db=f(W,x)。

1 def affine_forward(x, w, b): 2 ####################################### 3 # x: input shape (N, d_1, ..., d_k) 4 # w: Weights shape (D, M) 5 # b: bias shape (M) 6 # 7 # Returns : 8 # out: output,shape (N, M) 9 # cache: (x, w, b) 10 ######################################## 11 x1=x.reshape(x.shape[0],-1) 12 out=np.dot(x1,w)+b 13 cache=(x,w) 14 return out,cache 15 16 def affine_backward(dout, cache): 17 ################################################## 18 # dout: Upstream derivative, of shape (N, M) 19 # cache: Tuple of: 20 # x: Input shape (N, d_1, ... d_k) 21 # w: Weights shape (D, M) 22 # 23 # Returns a tuple of: 24 # dx: shape (N, d1, ..., d_k) 25 # dw: shape (D, M) 26 # db: shape (M,) 27 ################################################## 28 x,w=cache 29 N=x.shape[0] 30 x1=x.reshape(N,-1) 31 dx=np.dot(dout,w.T).reshape(*x.shape) 32 dw=np.dot(x1.T,dout) 33 db=np.sum(dout,axis=0) 34 return dx,dw,db
2、激活層
激活層只實現常用的relu激活函數。f(x)=max(0,x)。
反向傳播也很簡單,被激活的繼承上一級的梯度,沒有激活的梯度為0。這里不涉及到參數的梯度計算,只涉及到梯度的傳播。

1 def relu_forward(x): 2 out = np.maximum(0,x) 3 cache = x 4 return out, cache 5 6 7 def relu_backward(dout, cache): 8 x = cache 9 dx = dout * (x>0) 10 return dx
實際使用過程中,一般全連接層后面都會接激活層,為了方便起見,可以合並兩層。這里的合並跟之后的整個神經網絡的堆疊思想一致。

1 def affine_relu_forward(x, w, b): 2 """ 3 全連接層和激活層的合並 4 5 Inputs: 6 - x: 全連接層的輸入 7 - w, b: 全連接層的權重參數 8 9 Returns a tuple of: 10 - out: 被激活的全連接層的輸出 11 - cache: 用於反向傳播 12 """ 13 a,fc_cache=affine_forward(x,w,b) 14 out,relu_cache=relu_forward(a) 15 return out, (fc_cache,relu_cache) 16 17 18 def affine_relu_backward(dout, cache): 19 fc_cache, relu_cache = cache 20 da=relu_backward(dout,relu_cache) 21 dx,dw,db=affine_backward(da,fc_cache) 22 return dx, dw, db
3、loss層
嚴格上講這不算神經網絡的一個層,只是為了訓練而必須進行的一個計算,但是這里我們就把它也當作一個層好了。
loss函數在上一個博客中已經詳細介紹過了。https://www.cnblogs.com/super-JJboom/p/9748468.html

1 def svm_loss(x, y): 2 3 N,C=x.shape 4 correct_class_scores=x[range(N),y].reshape(-1,1) 5 margins=np.maximum(0,x-correct_class_scores+1) 6 loss=np.sum(margins)/N 7 dx = np.zeros_like(x) 8 dx[margins>0]=1 9 num_pos = np.sum(margins > 0, axis=1) 10 dx[range(N),y]-=num_pos 11 dx/=N 12 13 return loss, dx 14 15 16 def softmax_loss(x, y): 17 18 N,C=x.shape 19 shift_x=x-np.max(x,axis=1,keepdims=True).reshape(-1,1) 20 Z=np.sum(np.exp(shift_x),axis=1,keepdims=True) 21 log_pro=-shift_x+np.log(Z) 22 loss=np.sum(log_pro[range(N),y])/N 23 probs=np.exp(-log_pro) 24 probs[range(N),y]-=1 25 dx=probs 26 dx/=N
到此為止,之前我們實現兩層神經網絡需要的層已經都實現了。先重構一下之前的實現吧。

1 #2層神經網絡 2 class TwoLayerNet(object): 3 #The architecure : affine - relu - affine - softmax. 4 def __init__(self, input_dim=3*32*32,hidden_dim=100,num_classes=10,weight_scale=1e-3,reg=0.0): 5 ################################ 6 # input_dim 輸入維度 7 # hidden_dims 隱藏層神經元個數 8 # num_classes 輸出個數 9 # weight_scale 初始化權重 10 # reg 正則項系數 11 ################################ 12 self.params={} 13 self.reg=reg 14 self.params['W1']=weight_scale*np.random.randn(input_dim,hidden_dim) 15 self.params['b1']=np.zeros(hidden_dim) 16 self.params['W2']=weight_scale*np.random.randn(hidden_dim,num_classes) 17 self.params['b2']=np.zeros(num_classes) 18 19 def loss(self,X,y=None): 20 #返回loss和grad 21 22 #前向計算 23 ar1_out,ar1_cache=affine_relu_forward(X,self.params['W1'],self.params['b1']) 24 a2_out,a2_cache=affine_forward(ar1_out,self.params['W2'],self.params['b2']) 25 scores=a2_out 26 27 if y is None: 28 return scores 29 30 loss,grads=0,{} 31 loss,dscores=softmax_loss(scores,y) 32 loss=loss+0.5*self.reg*(np.sum(self.params['W1']**2)+np.sum(self.params['W2']**2)) 33 dx2,dw2,db2=affine_backward(dscores,a2_cache) 34 grads['W2']=dw2+self.reg*self.params['W2'] 35 grads['b2']=db2 36 37 dx1,dw1,db1=affine_relu_backward(dx2,ar1_cache) 38 grads['W1']=dw1+self.reg*self.params['W1'] 39 grads['b1']=db1 40 41 return loss,grads
看起來比之前的實現並沒有簡單多少。。。這是因為2層神經網絡的結構過於簡單,僅從代碼量上來看並沒有減少,但是對於后面要實現的更復雜的神經網絡來說,就發揮了巨大的作用。
哦,對比之前的實現,發現少了自動化訓練的實現。因為訓練有很多參數可以選擇和調節,之前沒有實現,如果全部放入神經網絡的類中的話會顯得過於臃腫,所以把訓練過程的實現單獨拿出來作為一個類。
4、自動化訓練
相比與之前的自動訓練過程,這里增加了更多的可選項。可以選擇優化方法,如:SGD,帶動量的SGD,adam。每一輪數據迭代完之后顯示數據。

1 import numpy as np 2 from cs231n import optim 3 4 class Solver(object): 5 def __init__(self,model,data,**kwargs): 6 ''' 7 初始化對象 8 inputs: 9 - model:網絡結構對象 10 - data:字典,包含帶標簽的訓練集和驗證集 11 - kwargs:可選參數,詳細見下面提取時候的注釋 12 ''' 13 14 self.model=model 15 self.X_train=data['X_train'] 16 self.y_train=data['y_train'] 17 self.X_val=data['X_val'] 18 self.y_val=data['y_val'] 19 20 #解讀kwargs 21 self.update_rule=kwargs.pop('update_rule','sgd') #優化方法的選擇,默認為隨機梯度下降 22 self.optim_config=kwargs.pop('optim_config',{}) #優化的參數,學習率是必須有的選項。其他可以有動量因子之類的參數 23 self.lr_decay=kwargs.pop('lr_decay',1.0) #學習率衰減因子,默認不衰減 24 self.batch_size=kwargs.pop('batch_size',128) #批大小,默認128 25 self.num_epochs=kwargs.pop('num_epochs',10) #訓練全部數據的輪次,默認為10輪 26 self.print_every=kwargs.pop('print_every',10) #多少輪顯示一次進度 27 self.verbose=kwargs.pop('verbose',True) #是否顯示進度,為false的情況下上一個參數無效 28 29 #含有不支持的參數 30 if len(kwargs)>0: 31 extra=','.join('"%s"' % k for k in kwargs.keys()) 32 raise ValueError('Unrecongnized arguments %s' %extra) 33 34 #檢查優化方法是否支持 35 if not hasattr(optim,self.update_rule): 36 raise ValueError('invalid update_rule "%s"' %self.update_rule) 37 38 self.update_rule=getattr(optim,self.update_rule) 39 40 self._reset() 41 42 def _reset(self): 43 #初始化參數 44 self.epoch=0 45 self.best_val_acc=0 46 self.best_params={} 47 self.loss_history=[] 48 self.train_acc_history=[] 49 self.val_acc_history=[] 50 51 #給給個參數矩陣復制一個優化參數,因為之后每個權重的參數不相同,需要自己保存 52 self.optim_configs={} 53 for p in self.model.params: 54 d={k:v for k,v in self.optim_config.items()} 55 self.optim_configs[p]=d 56 57 def _step(self): 58 #單步更新 59 60 #隨機取出batchsize個數據 61 num_train=self.X_train.shape[0] 62 batch_mask=np.random.choice(num_train,self.batch_size) 63 X_batch=self.X_train[batch_mask] 64 y_batch=self.y_train[batch_mask] 65 66 #計算loss 67 loss,grads=self.model.loss(X_batch,y_batch) 68 self.loss_history.append(loss) 69 70 #更新參數 71 for p,w in self.model.params.items(): 72 dw=grads[p] 73 config=self.optim_configs[p] 74 next_w,next_config=self.update_rule(w,dw,config) 75 self.model.params[p]=next_w 76 self.optim_configs[p]=next_config 77 78 #計算正確率 79 def check_accuracy(self,X,y,num_samples=None,batch_size=128): 80 N=X.shape[0] 81 82 #如果num_sample不為空 則只從全部數據中選則num_sample個數據計算 83 if num_samples is not None and N>num_samples: 84 mask=np.random.choice(N,num_samples) 85 N=num_samples 86 X=X[mask] 87 y=y[mask] 88 89 num_batches=N//batch_size 90 if N%batch_size!=0: 91 num_batches+=1 92 y_pred=[] 93 for i in range(num_batches): 94 start=i*batch_size 95 end=(i+1)*batch_size 96 scores=self.model.loss(X[start:end]) 97 y_pred.append(np.argmax(scores,axis=1)) 98 y_pred=np.concatenate(y_pred,axis=0) 99 acc=np.mean(y_pred==y) 100 101 return acc 102 103 def train(self): 104 num_train=self.X_train.shape[0] 105 iterations_per_epoch=max(num_train//self.batch_size,1) 106 num_iterations=self.num_epochs*iterations_per_epoch 107 108 for t in range(num_iterations): 109 self._step() 110 111 if self.verbose and t%self.print_every==0: 112 print('Iteration %d /%d loss: %f' %(t+1,num_iterations,self.loss_history[-1]) ) 113 114 #每個epoch執行相應操作 115 epoch_end=(t+1)%iterations_per_epoch==0 116 if epoch_end: 117 self.epoch+=1 118 for k in self.optim_configs: 119 self.optim_configs[k]['learning_rate']*=self.lr_decay 120 121 first_it=(t==0) 122 last_it=(t==num_iterations-1) 123 if first_it or last_it or epoch_end: 124 train_acc=self.check_accuracy(self.X_train,self.y_train,num_samples=1280) 125 val_acc= self.check_accuracy(self.X_val ,self.y_val) 126 self.train_acc_history.append(train_acc) 127 self.val_acc_history.append(val_acc) 128 129 #可視化進度 130 if self.verbose: 131 print ('(Epoch %d / %d) train acc: %f; val_acc: %f' % ( 132 self.epoch, self.num_epochs, train_acc, val_acc)) 133 134 #檢查、保存模型 135 if val_acc>self.best_val_acc: 136 self.best_val_acc=val_acc 137 self.best_params={} 138 for k,v in self.model.params.items(): 139 self.best_params[k]=v.copy() 140 141 self.model.params=self.best_params
實現到這里,已經可以重新訓練之前的兩層神經網絡了,訓練代碼全部整合帶最后的測試代碼里面了。
5、實現全連層神經網絡框架
實現跟兩層神經網絡區別不大,只是網絡層的堆疊使用了循環。這里還沒有實現的批歸一化和dropout操作后面會講到。

1 class FullyConnectedNet(object): 2 #archtecture: {affine - [batch norm] - relu - [dropout]} x (L - 1) - affine - softmax 3 def __init__(self,hidden_dims,input_dim=3*32*32,num_classes=10,dropout=0, 4 use_batchnorm=False,reg=0.0,weight_scale=1e-3,dtype=np.float32,seed=None): 5 6 ''' 7 inputs: 8 - hidden_dims:list,存儲了有多少個中間層,每一層有多少個神經元 9 - input_dim: 輸入數據的維度大小 10 - num_classes:類別的個數,也就是最后一層的神經元個數 11 - dropout:失活率 12 - use_batchnorm:是否在每一層之間使用批歸一化操作 13 - reg:正則權重 14 - weight_scale:權重矩陣的初始數量級 15 - seed:失活率隨機 16 ''' 17 18 self.use_batchnorm=use_batchnorm 19 self.use_dropout=(dropout>0) 20 self.reg=reg 21 self.num_layers=1+len(hidden_dims) 22 self.dtype=dtype 23 self.params={} 24 25 #初始化每層的參數w,b [gamma,beta,dropout](如果有的話) 26 layer_input=input_dim 27 for i,hd in enumerate(hidden_dims): 28 self.params['W%d'%(i+1)]=weight_scale*np.random.randn(layer_input,hd) 29 self.params['b%d'%(i+1)]=weight_scale*np.zeros(hd) 30 if self.use_batchnorm: 31 self.params['gamma%d'%(i+1)]=np.ones(hd) 32 self.params['beta%d'%(i+1)]=np.zeros(hd) 33 layer_input=hd 34 self.params['W%d'%(self.num_layers)]=weight_scale*np.random.randn(layer_input,num_classes) 35 self.params['b%d'%(self.num_layers)]=weight_scale*np.zeros(num_classes) 36 for k,v in self.params.items(): 37 self.params[k]=v.astype(dtype) 38 39 self.dropout_param={} 40 if self.use_dropout: 41 self.dropout_param={'mode':'train','p':dropout} 42 if seed is not None: 43 self.dropout_param['seed']=seed 44 45 self.bn_params=[] 46 if self.use_batchnorm: 47 self.bn_params=[{'mode':'train'} for i in range(self.num_layers-1)] 48 49 def loss(self,X,y=None): 50 51 #跟之前一樣,y=None時表示測試過程,直接返回最后一層的輸出即可。否則表示訓練過程,還要計算loss和gradient。 52 53 X=X.astype(self.dtype) 54 mode='test' if y is None else 'train' 55 56 if self.dropout_param is not None: 57 self.dropout_param['mode'] = mode 58 if self.use_batchnorm: 59 for bn_param in self.bn_params: 60 bn_param['mode'] = mode 61 62 63 #forward pass 64 layer_input=X 65 ar_cache={} 66 dp_cache={} 67 68 for lay in range(self.num_layers-1): 69 if self.use_batchnorm: 70 layer_input, ar_cache[lay] = affine_bn_relu_forward(layer_input, 71 self.params['W%d'%(lay+1)], self.params['b%d'%(lay+1)], 72 self.params['gamma%d'%(lay+1)], self.params['beta%d'%(lay+1)], self.bn_params[lay]) 73 else: 74 layer_input,ar_cache[lay]=affine_relu_forward(layer_input,self.params['W%d'%(lay+1)],self.params['b%d'%(lay+1)]) 75 76 if self.use_dropout: 77 layer_input, dp_cache[lay] = dropout_forward(layer_input, self.dropout_param) 78 79 ar_out,ar_cache[self.num_layers]=affine_forward(layer_input,self.params['W%d'%(self.num_layers)],self.params['b%d'%(self.num_layers)]) 80 scores=ar_out 81 82 #預測時直接返回scores即可 83 if mode=='test': 84 return scores 85 86 #訓練時還要計算loss和gradient 87 grads={} 88 loss,dscores=softmax_loss(scores,y) 89 dhout=dscores 90 loss+=0.5*self.reg*np.sum(self.params['W%d'%(self.num_layers)]**2) 91 dx,dw,db=affine_backward(dhout,ar_cache[self.num_layers]) 92 grads['W%d'%(self.num_layers)]=dw+self.reg*self.params['W%d'%(self.num_layers)] 93 grads['b%d'%(self.num_layers)]=db 94 dhout=dx 95 for lay in range(self.num_layers-1): 96 lay=self.num_layers-1-lay-1 97 loss+=0.5*self.reg*np.sum(self.params['W%d'%(lay+1)]**2) 98 if self.use_dropout: 99 dout=dropout_backward(dhout,dp_cache[lay]) 100 if self.use_batchnorm: 101 dx,dw,db,dgamma,dbeta=affine_bn_relu_backward(dhout,ar_cache[lay]) 102 grads['gamma%d'%(lay+1)] = dgamma 103 grads['beta%d'%(lay+1)] = dbeta 104 else: 105 dx,dw,db=affine_relu_backward(dhout,ar_cache[lay]) 106 grads['W%d'%(lay+1)]=dw+self.reg*self.params['W%d'%(lay+1)] 107 grads['b%d'%(lay+1)]=db 108 dhout=dx 109 110 return loss,grads