NLP鱼书学习笔记1-神经网络推理

2023-01-28 23:47:27 浏览数 (2)

公众号:尤而小屋 作者:Peter 编辑:Peter

大家好,我是Peter~

本文记录的是书籍《深度学习进阶:自然语言处理》的第一章学习笔记。

这本书简称为鱼书,朋友推荐的NLP经典书籍。后面会陆续更新读书笔记。

神经网络推理

$(x1,x_2)$表示输入层的数据,$w{11}、w_{21}$表示权重,$b_1$表示偏置。

第一个隐藏神经元的结果可以表示为:

$$h1 = x_1w{11} x2w{21} b_1$$

隐藏层的神经元是基于加权和计算出来的。

python实现mini版全连接

代码语言:python代码运行次数:0复制
import numpy as np

W1 = np.random.randn(2, 4)  # 2*4
b1 = np.random.randn(4) 
x = np.random.randn(10,2)  # 10*2
h = np.dot(x, W1)   b1

print("W1:n",W1)
print("b1:n",b1)
print("x:n",x)
print("h:n",h)
代码语言:txt复制
W1:
代码语言:txt复制
 [[ 1.2395529   1.36244947 -0.36417116  1.37209539]
代码语言:txt复制
 [ 0.79381936  0.9539435   0.10809906  1.20005854]]
代码语言:txt复制
b1:
代码语言:txt复制
 [ 0.90337564 -0.37367157 -0.65477654 -0.53367142]
代码语言:txt复制
x:
代码语言:txt复制
 [[-1.87632050e-01 -1.56664439e-01]
代码语言:txt复制
 [-1.77483807e 00  1.14098853e 00]
代码语言:txt复制
 [ 1.74651236e 00 -9.99758195e-01]
代码语言:txt复制
 [-3.18839224e-01 -9.41541718e-06]
代码语言:txt复制
 [-9.05030740e-01 -4.27663837e-01]
代码语言:txt复制
 [ 5.08077914e-01  9.23716486e-01]
代码语言:txt复制
 [-1.13378280e 00  1.71743576e 00]
代码语言:txt复制
 [-1.07155261e 00 -4.37605821e-01]
代码语言:txt复制
 [-9.05201730e-02 -3.37833720e-01]
代码语言:txt复制
 [ 9.45202380e-01 -5.44267793e-01]]
代码语言:txt复制
h:
代码语言:txt复制
 [[ 0.54643252 -0.77875978 -0.60338164 -0.97912699]
代码语言:txt复制
 [-0.39089126 -1.70336018  0.11490808 -1.59966553]
代码语言:txt复制
 [ 2.2746427   1.05215044 -1.39887889  0.66294178]
代码语言:txt复制
 [ 0.50815008 -0.80808289 -0.53866551 -0.97116055]
代码语言:txt复制
 [-0.55794567 -2.01469736 -0.37142051 -2.28868157]
代码语言:txt复制
 [ 2.26642912  1.19973225 -0.73995098  1.2719738 ]
代码语言:txt复制
 [ 0.86132563 -0.28005668 -0.05623236 -0.02830612]
代码语言:txt复制
 [-0.77225049 -2.25105909 -0.31185276 -2.52909643]
代码语言:txt复制
 [ 0.52299215 -0.81927501 -0.65833121 -1.06329398]
代码语言:txt复制
 [ 1.64295368  0.39491819 -1.05782682  0.11008319]]

上面的例子中有10笔样本数据:$x0、x1......$,对应10个隐藏层的神经元$h0、h1......$

使用sigmoid激活函数

全连接层的变换是线性变换,激活函数能够赋予它"非线性"的效果。使用激活函数能够增加神经网络的表现力。

代码语言:python代码运行次数:0复制
def sigmoid(x):
    return 1 / (1 np.exp(-x))
代码语言:python代码运行次数:0复制
a = sigmoid(h)  # 对隐层单元的内容使用激活函数

a
代码语言:txt复制
array([[0.63330751, 0.31458724, 0.35357041, 0.27306504],
代码语言:txt复制
       [0.40350277, 0.15402692, 0.52869545, 0.16802837],
代码语言:txt复制
       [0.90675507, 0.74118763, 0.19799407, 0.65992091],
代码语言:txt复制
       [0.62437271, 0.30829917, 0.36849807, 0.27464924],
代码语言:txt复制
       [0.36402292, 0.11766841, 0.40819782, 0.0920647 ],
代码语言:txt复制
       [0.90605829, 0.76847715, 0.32301486, 0.78108044],
代码语言:txt复制
       [0.70293754, 0.43043988, 0.48594561, 0.49292394],
代码语言:txt复制
       [0.31599248, 0.09525815, 0.42266257, 0.07384342],
代码语言:txt复制
       [0.62784716, 0.30591758, 0.34111458, 0.25668048],
代码语言:txt复制
       [0.83793645, 0.59746609, 0.25772497, 0.52749304]])

用另一个全连接层来变换这个激活函数的输出$a$。

隐藏层有4个神经元,输出层有3个神经元,所以全连接层使用的权重矩阵的形状设置成$4*3$。

完整代码

整体的完整代码为:

代码语言:python代码运行次数:0复制
import numpy as np

def sigmoid(x):
    return 1 / (1 np.exp(-x))

x = np.random.randn(10,2)  # 10*2
W1 = np.random.randn(2, 4)  # 2*4  # 4表示隐藏神经元个数;2是和输入x的x.shape[1]相匹配
b1 = np.random.randn(4) # 10*4;偏置必须为4
W2 = np.random.randn(4, 3)  # 4*3  # 3个输出神经元个数;4个第一个隐藏神经元的shape[1]
b2 = np.random.randn(3) #  # 10*3;偏置必须为3
h = np.dot(x, W1)   b1

a = sigmoid(h)
s = np.dot(a, W2)   b2
s
代码语言:txt复制
array([[ 2.1993881 , -0.66932037,  0.73650529],
代码语言:txt复制
       [ 2.10594575, -0.44002399,  0.67983363],
代码语言:txt复制
       [ 1.54631603,  0.46590836,  0.48583189],
代码语言:txt复制
       [ 1.91380546,  0.09817995,  0.50751596],
代码语言:txt复制
       [ 1.34782105,  0.98687129,  0.37225228],
代码语言:txt复制
       [ 2.18438808, -0.63015213,  0.7306245 ],
代码语言:txt复制
       [ 1.8960649 ,  0.13953076,  0.49930039],
代码语言:txt复制
       [ 1.36793674,  1.02409436,  0.37293796],
代码语言:txt复制
       [ 1.90386465,  0.00908374,  0.5740634 ],
代码语言:txt复制
       [ 1.16277154,  1.05478025,  0.30068793]])

python实现层的类及正向传播

  • sigmoid函数的变换:Sigmoid层
  • 全连接层的变换相当于几何学领域的放射变换:Affine层

代码规范:

  1. 所有的层都使用forward()方法和backward()方法
  2. 所有的层都使用params 和 grads实例变量

Sigmoid层

代码语言:python代码运行次数:0复制
import numpy as np

class Sigmoid:
    def __init__(self):
        self.params = []   # 没有学习的参数,使用空列表
        
    def forward(self,x):
        return 1 / (1   np.exp(-x))

Affine层

代码语言:python代码运行次数:0复制
class Affine:
    def __init__(self, W, b):
        # 初始化时接收权重和偏置
        self.params = [W,b]  # 参数列表保存
        
    def forward(self, x):
        W,b = self.params
        out = np.dot(x,W)   b
        return out

TwoLayerNet网络

代码语言:python代码运行次数:0复制
class TwoLayerNet:
    def __init__(self, input_size, hidden_size, output_size):
        I,H,O = input_size, hidden_size, output_size
        
        W1 = np.random.randn(I,H)
        b1 = np.random.randn(H)
        W2 = np.random.randn(H,O)
        b2 = np.random.randn(O)
        
        # 生成层
        self.layers = [
            Affine(W1, b1),  
            Sigmoid(),
            Affine(W2,b2)
        ]
        
        # 将所有的权重整理到列表中
        self.params = []
        
        for layer in self.layers:  # 循环每个层;权重参数放到列表params中
            self.params  = layer.params    
            
    def predict(self, x):
        for layer in self.layers:
            x = layer.forward(x)  # 对每个层使用forward的更新方法,输出Out
        return x

应用

代码语言:python代码运行次数:0复制
x = np.random.randn(10,2)
model = TwoLayerNet(2,4,3)

s = model.predict(x)
s
代码语言:txt复制
array([[-0.89091485,  0.3790137 , -0.69215058],
代码语言:txt复制
       [ 0.31672348,  0.33206884, -1.32879037],
代码语言:txt复制
       [-1.04059946,  0.69280576, -0.71941753],
代码语言:txt复制
       [ 2.07021878, -0.74625453, -2.63285994],
代码语言:txt复制
       [ 0.8618358 ,  0.24588236, -1.69390253],
代码语言:txt复制
       [-0.36771902,  0.68768811, -0.99495204],
代码语言:txt复制
       [-0.26414073,  0.39642317, -1.00626304],
代码语言:txt复制
       [-0.08078445,  0.15941385, -1.14579999],
代码语言:txt复制
       [-0.66130715,  0.48855694, -0.8163898 ],
代码语言:txt复制
       [ 0.74677525,  0.32622056, -1.6045171 ]])

神经网络的学习

损失函数loss function

基于监督学习或者神经网络的预测结果,与实际结果之间差异程度。也就说将模型的恶劣程度作为标量值计算出来,得到的就是损失。

多类别分类问题中,通常使用的交叉熵误差cross entropy 作为损失函数。

Softmax函数的表达式:

$$yk = frac{e^{S{k}}}{sum_{i=1}^n* e^{S_i}}$$

Softmax函数输出的各个元素是0.0~1.0的实数。如果将这些元素全部加起来,则和为1.因此,Softmax的输出可以解释为概率。之后这个概率被输入交叉熵误差中。

交叉熵误差表示为:

$$L=-sumkt_klogy{k}$$

  • $t_k$对应于第k个类别的监督标签
  • log是以e为底数的对数

在考虑了mini-batch处理的情况下,交叉熵误差可以表示为:

$$L=-frac{1}{N}sumnsum_kt{nk}logy_{nk}$$

假设有N笔数据,$t{nk}$表示第n笔数据的第k维元素的值;$y{nk}$表示神经网络的输出,$t_{nk}$表示监督标签

MatMul层的实现

代码语言:python代码运行次数:0复制
class MatMul:
    def __init__(self, W):
        self.params = [W]  # 保存学习的参数
        self.grads = [np.zeros_like(W)]  # 梯度保存在grads
        self.x = None
    
    # 前向传播
    def forward(self, x):
        W, = self.params    # 参数
        out = np.dot(x,W)   # 输出
        self.x = x
        return out
    
    # 后向传播
    def backword(self, dout):
        W, = self.params
        dx = np.dot(dout, W.T)
        dW = np.dot(self.x.T, dout)
        # grads[0][...] 使用了省略号:可以固定Numpy数组的内存地址,覆盖Numpy数组的元素
        self.grads[0][...] = dW  # 实例变量grads中设置权重的梯度
        return dx

关于Numpy的...复制问题

代码语言:python代码运行次数:0复制
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
代码语言:python代码运行次数:0复制
id(a),id(b)
代码语言:txt复制
(1549258733968, 1549258615664)
代码语言:python代码运行次数:0复制
a = b  #  将b赋值给a;
a
代码语言:txt复制
array([4, 5, 6])

可以看到a和b的内存地址是完全相同的;

代码语言:python代码运行次数:0复制
id(a),id(b)
代码语言:txt复制
(1549258615664, 1549258615664)
代码语言:python代码运行次数:0复制
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
代码语言:python代码运行次数:0复制
id(a),id(b)
代码语言:txt复制
(1549258733392, 1549258809392)
代码语言:python代码运行次数:0复制
a[...] = b  # 赋值
代码语言:python代码运行次数:0复制
id(a),id(b)
代码语言:txt复制
(1549258733392, 1549258809392)

可以看到a和b的内存地址是不同的;且a的地址还是赋值前的地址。

总结规律:使用省略号时数据会被覆盖,变量指向的内存地址不变。

梯度的推导和反向传播实现

Sigmoid层
代码语言:python代码运行次数:0复制
class Sigmoid:
    def __init__(self):
        self.params, self.grads = [], []  # 参数和梯度的保存
        self.out = None
        
    def forward(self, x):
        # 前向传播过程
        out = 1 / (1   np.exp(-x))  # sigmoid函数
        self.out = out   #  保存输出out
        return out
    
    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out
        return dx
Affine层

通过$y = np.dot(x,W) b$实现了Affine层的正向传播

代码语言:python代码运行次数:0复制
class Affine:
    def __init__(self, W, b):
        self.params = [W,b]  # 保存参数
        self.grads = [np.zeros_like(W), np.zeros_like(W)]  # 梯度初始化
        self.x = None
        
    def forward(self, x):
        W,b = self.params
        out = np.dot(x,W)   b  # 前向输出
        self.x = x
        return out
    
    def backword(self, dout):
        W, b = self.params
        dx = np.dot(dout, W.T)  # 
        dW = np.dot(self.x.T, dout)
        db = np.sum(dout, axis=0)
        
        
        self.grads[0][...] = dW
        self.grads[1][...] = db
        return dx
权重更新
代码语言:python代码运行次数:0复制
class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr  # 学习率设置
    
    def update(self, params, grads):
        for i in range(len(params)):
            params[i] -= self.lr * grads[i]  # 参数更新

使用SGD类更新神经网络的参数:

代码语言:txt复制
# 伪代码

model = TwoLayerNet(...)
optimizer = SGD()

for i in range(10000):
   x_batch, t_batch = get_mini_batch()
   loss = model.farward(x_batch, t_batch)
   model.backward()
   optimizer.update(model.params, model.grads)

0 人点赞