深度學習入門——基於numpy(一)

深度學習入門——基於numpy(一)

說明:本次練習代碼均來自齋藤康毅的《深度學習入門》 ,全文不依賴其他框架,只基於numpy來構建深度學習網絡。

一、激活函數

說明:激活函數相當於是神經網絡的神經元,他們根據輸入信號,反饋輸出信號。因此不同類型的激活函數會構建成不同類型、不同敏感度的神經網絡。

# 導入包
import numpy as np
# 導入作圖包
import matplotlib
import matplotlib.pylab as plt
matplotlib.rcParams['font.sans-serif'] = ['SimHei']
matplotlib.rcParams['font.family']='sans-serif'
plt.rcParams['axes.unicode_minus'] = False 
# 忽略警告
import warnings
warnings.filterwarnings('ignore')
# 激活函數,階躍函數
def step_function(x):
    return np.array(x>0, dtype=np.int)
# 激活函數,連續型,每一處導數均不爲0,因此每一個x值都有一個方向以及程度上的反饋信號,因此可以通過反饋信號,不斷更新我們的x,而階躍函數不具有此性質
def sigmoid(x):
    return 1/(1+np.exp(-x))
x=np.arange(-5,5,0.2)
# 畫出階躍函數
y1=step_function(x)
plt.plot(x,y1)
plt.show()

# 畫出sigmoid函數
y2=sigmoid(x)
plt.plot(x,y2)
plt.show()

在這裏插入圖片描述

# relu函數
def relu(x):
    return np.maximum(0,x)
y3=relu(x)
plt.plot(x,y3)
plt.show()

在這裏插入圖片描述

二、輸出層

說明:神經網絡一般有輸入層、中間層(也叫隱藏層)、輸出層構成。輸出層負責獲得最終的計算結果,一般是一個概率值。

# softmax函數,可將任意範圍的數轉換到值域[0,1],因此適合作爲輸出結果的概率函數。
def softmax(x):
    if x.ndim == 2:
        x = x.T # 轉置後一行表示一個特徵向量
        x = x - np.max(x, axis=0) # 在該特徵向量上,每個特徵值減去最大的特徵值
        y = np.exp(x) / np.sum(np.exp(x), axis=0) 
        return y.T # 計算完後再轉置回來,每行表示一個樣本

    x = x - np.max(x) # 溢出對策
    return np.exp(x) / np.sum(np.exp(x))
x=np.array([10,20,40]) #x.ndim=1
print(softmax(x))
x=np.array([[20,2,3],[40,3,1]]) # x.ndim=2
print(softmax(x))
[9.35762295e-14 2.06115362e-09 9.99999998e-01]
[[9.99999943e-01 1.52299789e-08 4.13993748e-08]
 [1.00000000e+00 8.53304763e-17 1.15482242e-17]]

三、損失函數

說明:神經網絡需要更新神經元之間連接的權重參數,以使得最終網絡預測的結果與真實結果是相近的。因此,整個網絡需要有一個優化目標來更新這些參數,我們一般定義計算的結果與真實結果的差異作爲優化的目標。這種差異可以有多種定義方式,比如誤差、均方誤差、交叉熵等。

# 損失函數,均方誤差,y爲神經網絡的輸出,t爲真實標籤
def mean_squared_error(y,t):
    return 0.5 * np.sum((y-t)**2)
# 損失函數,交叉熵,y爲神經網絡的輸出,t爲真實標籤,若t爲one-hot編碼,假設結果標籤是數字1-5的值,則[0,1,0,0,0]表示這個樣本的真實值是2。因此t爲0的元素其交叉熵也爲0,可用t*np.log(y+1e-7)來計算
def cross_entropy_error_onehot(y,t):
    if y.ndim==1:
        t=t.reshape(1,t.size) # 1行,t.size列
        y=y.reshape(1,y.size)
    batch_size=y.shape[0] # y的行數
    return -np.sum(t*np.log(y+1e-7))/batch_size # 除以batch_size是爲了歸一化,得到數據的平均交叉熵誤差
# 示例數據,y 爲(batch_size,3),t爲(batch_size,3), batch_size表示樣本數,此處爲4,3則表示有3種結果
y=np.array([[1,0,0],[0,1,0],[1,0,0],[0,0,1]])
t=np.array([[1,0,0],[0,1,0],[1,0,0],[0,0,1]])
# 計算損失函數的值
print("y的維數:",y.ndim)
print("第一個樣本的y:",y[0])
print("第一個樣本的t:",t[0])
print("樣本的個數:",y.shape[0])
print("y的總長度:",y.size)
print("損失函數的值:",cross_entropy_error_onehot(y,t))
y的維數: 2
第一個樣本的y: [1 0 0]
第一個樣本的t: [1 0 0]
樣本的個數: 4
y的總長度: 12
損失函數的值: -9.999999505838704e-08
# 損失函數,交叉熵,若y爲one-hot編碼,但t爲非one-hot編碼。
def cross_entropy_error_notonehot(y,t):
    if y.ndim==1:
        t=t.reshape(1,t.size)
        y=y.reshape(1,y.size)
    print("t.shape:",t.shape)
    batch_size=y.shape[0] 
    print(np.arange(batch_size)) # [0 1 2 3]
    print(y[np.arange(batch_size),t]) # [0 1 1 1]
    return -np.sum(np.log(y[np.arange(batch_size),t]+1e-7))/batch_size
# 示例數據,y 爲(batch_size,3),t爲(batch_size,),t是一維的
y=np.array([[1,0,0],[0,1,0],[1,0,0],[0,0,1]])
t_onehot=np.array([[0,1,0],[0,1,0],[1,0,0],[0,0,1]])
t = t_onehot.argmax(axis=1)# 非 one-hot [1 1 0 2]
# 計算損失函數的值
print("損失函數的值:",cross_entropy_error_notonehot(y,t))
t.shape: (4,)
[0 1 2 3]
[0 1 1 1]
損失函數的值: 4.029523837739585
# 最終損失函數
def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    # 監督數據是one-hot-vector的情況下,轉換爲正確解標籤的索引
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
# 示例數據,y 爲(batch_size,3),t爲(batch_size,),t是一維的
y=np.array([[1,0,0],[0,1,0],[1,0,0],[0,0,1]])
t_onehot=np.array([[0,1,0],[0,1,0],[1,0,0],[0,0,1]])
t = t_onehot.argmax(axis=1)# 非 one-hot [1 1 0 2]
# 計算損失函數的值
print("損失函數的值:",cross_entropy_error(y,t))
損失函數的值: 4.029523837739585

四、梯度

說明:損失函數告訴了我們網絡計算的結果與真實結果的差異,但是並沒有告訴我們應該如何更新權重參數。梯度即是定義參數變化對損失函數的影響值。我們最終希望得到最小的預測差異,也就是說在某些參數值附近,參數的變化對預測差異已經沒有多少影響了。梯度不斷的減小,最終趨於0時,我們就找到了那些參數。

# 方程1
def function_1(x):
    return 0.01*x**3 + 0.1*x
# 示例數據作圖
x=np.arange(0.0,20.0,0.1)
y=function_1(x)
plt.plot(x,y)
plt.xlabel("x")
plt.ylabel("y")
plt.title("0.01x^3+0.1x")
plt.show()

在這裏插入圖片描述

# 求導數
def numerical_diff(f,x):
    h=1e-4
    return (f(x+h)-f(x))/(2*h)
# 示例作圖
y1=numerical_diff(function_1,x)
plt.plot(x,y1)
plt.title("0.01x^3+0.1x的導數")
plt.show()

在這裏插入圖片描述

# 方程2
def function_2(x):
    return x[0]**2 + x[1]**2
    # 或者 return np.sum(x**2)
x=np.array([3,4])
function_2(x)
25
# 計算函數f在x處的梯度值,x是一個一維的數組,表示某一個x=[x0,x1,x2,x3...xn],n爲空間的維度
def numerical_gradient_1d(f,x):
    x=x.astype(float) # 轉爲float類型
    h=1e-4
    grad=np.zeros_like(x)
    
    for idx in range(x.size):
        tmp_val=x[idx]
        
        # f(x+h)計算
        x[idx]=tmp_val+h
        fxh1=f(x)
        
        # f(x-h)的計算
        x[idx]=tmp_val-h
        fxh2=f(x)
        #print("idx:{0},fxh1:{1},fxh2:{2}".format(idx,fxh1,fxh2))
        grad[idx]=(fxh1-fxh2)/(2*h)
        x[idx]=tmp_val
        
    return grad
numerical_gradient_1d(function_2,np.array([3,4]))
array([6., 8.])
# 如果 x是一個矩陣,則需要用到numpy自帶的迭代器nditer
def numerical_gradient(f,x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)
    
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 還原值
        it.iternext()   
        
    return grad
# 梯度下降法,每次更新x值,更新的程度就是x的梯度乘以一個係數,這樣梯度越大,x更新的就越大,最終梯度爲0附近時,x就不更新了。
def gradient_decent(f,init_x,lr=0.01,step_num=100):
    x = init_x
    xplot=np.zeros((step_num,2))
    for i in range(step_num):
        grad = numerical_gradient_1d(f,x)
        x -= lr*grad
        xplot[i,:]=x
    return xplot,x
# 用梯度下降法求f(x0+x1)=x0^2+x1^2的最小值
init_x=np.array([-3.,4.])
xplot,x=gradient_decent(function_2,init_x,lr=0.1,step_num=100)
print("最小值爲:{0}".format(x))
最小值爲:[-6.11110793e-10  8.14814391e-10]
# 畫出x更新的過程
plt.scatter(xplot[:,0],xplot[:,1])
plt.xlim(-3,3)
plt.ylim(-3,3)
plt.xlabel("x0")
plt.ylabel("x1")
plt.title("x0,x1的更新過程")
txt=np.array(100)
plt.show()

在這裏插入圖片描述

# 神經網絡的梯度。神經網絡的梯度定義爲損失函數關於權重參數的梯度。因爲我們最終求的是權重參數值,因此梯度定義爲權重參數的改變對損失函數的影響程度。
class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3) # 隨機初始化權重參數
    def predict(self,x):
        return np.dot(x, self.W)
    def loss(self,x,t):
        z = self.predict(x) # 預測值
        y = softmax(z) # 輸出層
        loss = cross_entropy_error(y,t) # 計算差異
        return loss
# 測試一下這個神經網絡
net = simpleNet()
print(">>>網絡連接的權重參數爲:\n{0}".format(net.W))
x=np.array([0.6,0.9])
p=net.predict(x)
print(">>>預測值爲:\n{0}".format(p))
print(">>>預測值的索引:\n{0}".format(np.argmax(p)))
t=np.array([1,0,0])
loss=net.loss(x,t)
print(">>>誤差爲:\n{0}".format(loss))
>>>網絡連接的權重參數爲:
[[-1.2113439   0.32843788  0.3827121 ]
 [-1.48413071 -1.21658075 -2.36810466]]
>>>預測值爲:
[-2.06252398 -0.89785994 -1.90166693]
>>>預測值的索引:
1
>>>誤差爲:
1.682569435059308
# 計算梯度,參數W即是之前的x,我們期望求的是Loss關於參數W的梯度
def f(W):
    return net.loss(x,t) # 函數定義爲,給一個參數W,即返回這個W對應的loss
dW = numerical_gradient(f,net.W)
print(">>>W處的梯度爲:\n{0}".format(dW))
>>>W處的梯度爲:
[[-0.48846237  0.3574599   0.13100247]
 [-0.73269355  0.53618985  0.19650371]]

五、整個學習過程的實現

說明:神經網絡的學習過程主要分爲,獲得訓練數據、計算訓練數據的損失函數、計算各權重參數的梯度、將權重參數更新、不斷重複以上步驟,達到某個條件後停止更新。

# 定義兩層的神經網絡
class TwoLayerNet:

    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        # 初始化權重
        # input_size 輸入層神經元的個數
        # hidden_size 中間層神經元的個數
        # output_size 輸出層神經元的個數
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size) # 第一層權重參數,即輸入與中間層之間的權重參數
        self.params['b1'] = np.zeros(hidden_size) # 第一層偏置參數
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size) # 第二層權重參數
        self.params['b2'] = np.zeros(output_size) # 第二層偏置參數

    def predict(self, x):
        # 根據輸入的x,計算預測值y
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
    
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        return y
        
    def loss(self, x, t):
        # 根據輸入x,真實的標籤t,計算差異值
        y = self.predict(x)
        
        return cross_entropy_error(y, t)
    
    def accuracy(self, x, t):
        # 統計預測的正確率
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)
        
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
        
    def numerical_gradient(self, x, t):
        # 根據輸入的x,真實的標籤t,計算各網絡參數對應的梯度grads
        loss_W = lambda W: self.loss(x, t) # 定義基於權重參數的損失函數loss_W
        
        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1']) # 根據損失函數,以及目前第一層的權重參數W1,計算W1對應的梯度
        grads['b1'] = numerical_gradient(loss_W, self.params['b1']) # 類似
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
        
        return grads
        
    def gradient(self, x, t):
        # 誤差反向傳播法
        # 根據輸入的x,真實的標籤t,計算各網絡參數對應的梯度grads,是numerical_gradient方法的高速版。
        # 先是根據輸入的x,真實的標籤t,向前傳播,計算預測值y
        # 根據預測值與真實值的差異,反向更新各層的參數對應的梯度值
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}
        
        batch_num = x.shape[0] # 輸入的一批樣本的個數
        
        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1) # 第一層神經元的輸出值
        a2 = np.dot(z1, W2) + b2 
        y = softmax(a2) # y爲預測值
        
        # backward
        dy = (y - t) / batch_num # 預測值與真實值的差異
        grads['W2'] = np.dot(z1.T, dy) # 根據差異值,第一層神經元的輸出值,計算第二層的權重參數
        grads['b2'] = np.sum(dy, axis=0) # 根據差異值,更新第二層的偏置參數
        
        da1 = np.dot(dy, W2.T) 
        dz1 = sigmoid_grad(a1) * da1 
        grads['W1'] = np.dot(x.T, dz1)
        grads['b1'] = np.sum(dz1, axis=0)

        return grads
# 測試二層的神經網絡的各個參數
net = TwoLayerNet(input_size=784,hidden_size=100,output_size=10)
print("W1的形狀:",net.params['W1'].shape)
print("b1的形狀:",net.params['b1'].shape)
print("W2的形狀:",net.params['W2'].shape)
print("b2的形狀:",net.params['b2'].shape)
W1的形狀: (784, 100)
b1的形狀: (100,)
W2的形狀: (100, 10)
b2的形狀: (10,)
# 測試各參數的梯度的計算結果
x=np.random.rand(100,784)
y=net.predict(x)
t=np.random.rand(100,10)
grads = net.numerical_gradient(x,t) # 計算時間較長,請耐心等待。
print("W1的梯度的形狀:",grads['W1'].shape)
print("b1的梯度的形狀:",grads['b1'].shape)
print("W2的梯度的形狀:",grads['W2'].shape)
print("b2的梯度的形狀:",grads['b2'].shape)
W1的梯度的形狀: (784, 100)
b1的梯度的形狀: (100,)
W2的梯度的形狀: (100, 10)
b2的梯度的形狀: (10,)
# 開始訓練
# 載入python自帶的數據集mnist
from dataset.mnist import load_mnist
(x_train,t_train),(x_test,y_test)=  load_mnist(normalize=True,one_hot_label=True) # 載入時間較長,請耐心等待
print(x_train.shape)
print(t_train.shape)
print(x_test.shape)
print(y_test.shape)
(60000, 784)
(60000, 10)
(10000, 784)
(10000, 10)
# 初始化以及參數設定
train_loss_list=[]
# 定義超參數。超參數與模型本身的參數不同,是與網絡參數學習相關的參數。
iters_num = 1000
train_size=x_train.shape[0]
batch_size=100 #每次採樣的個數
learning_rate=0.1
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
# 訓練過程
for i in range(iters_num):
    print("\r訓練進度:{0}%".format(int((i+1)/iters_num*100)),end="")
    # 採樣
    batch_mask = np.random.choice(train_size,batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # 計算mini-batch數據下的梯度
    # grad = network.numerical_gradient(x_batch,t_batch)
    grad = network.gradient(x_batch,t_batch) # 高速版
    
    # 更新參數
    for key in ('W1','b1','W2',"b2"):
        network.params[key] -= learning_rate * grad[key]
    
    # 記錄學習過程
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
訓練進度:100%
# 畫出損失函數的值的迭代圖像
plt.plot(train_loss_list)
plt.ylabel("損失函數的損失值")
plt.xlabel("迭代的輪數")
plt.show()

在這裏插入圖片描述

發佈了2 篇原創文章 · 獲贊 10 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章