Fork me on GitHub

NLP学习1-神经网络的推理

神经网络推理

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

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

$$h_1 = x_1w_{11} + x_2w_{21} + b_1$$

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

python实现mini版全连接

1
2
3
4
5
6
7
8
9
10
11
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)
W1:
 [[ 1.2395529   1.36244947 -0.36417116  1.37209539]
 [ 0.79381936  0.9539435   0.10809906  1.20005854]]
b1:
 [ 0.90337564 -0.37367157 -0.65477654 -0.53367142]
x:
 [[-1.87632050e-01 -1.56664439e-01]
 [-1.77483807e+00  1.14098853e+00]
 [ 1.74651236e+00 -9.99758195e-01]
 [-3.18839224e-01 -9.41541718e-06]
 [-9.05030740e-01 -4.27663837e-01]
 [ 5.08077914e-01  9.23716486e-01]
 [-1.13378280e+00  1.71743576e+00]
 [-1.07155261e+00 -4.37605821e-01]
 [-9.05201730e-02 -3.37833720e-01]
 [ 9.45202380e-01 -5.44267793e-01]]
h:
 [[ 0.54643252 -0.77875978 -0.60338164 -0.97912699]
 [-0.39089126 -1.70336018  0.11490808 -1.59966553]
 [ 2.2746427   1.05215044 -1.39887889  0.66294178]
 [ 0.50815008 -0.80808289 -0.53866551 -0.97116055]
 [-0.55794567 -2.01469736 -0.37142051 -2.28868157]
 [ 2.26642912  1.19973225 -0.73995098  1.2719738 ]
 [ 0.86132563 -0.28005668 -0.05623236 -0.02830612]
 [-0.77225049 -2.25105909 -0.31185276 -2.52909643]
 [ 0.52299215 -0.81927501 -0.65833121 -1.06329398]
 [ 1.64295368  0.39491819 -1.05782682  0.11008319]]

上面的例子中有10笔样本数据:$x[0]、x[1]…$,对应10个隐藏层的神经元$h[0]、h[1]…$

使用sigmoid激活函数

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

1
2
def sigmoid(x):
return 1 / (1+np.exp(-x))
1
2
3
a = sigmoid(h)  # 对隐层单元的内容使用激活函数

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

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

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

完整代码

整体的完整代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
array([[ 2.1993881 , -0.66932037,  0.73650529],
       [ 2.10594575, -0.44002399,  0.67983363],
       [ 1.54631603,  0.46590836,  0.48583189],
       [ 1.91380546,  0.09817995,  0.50751596],
       [ 1.34782105,  0.98687129,  0.37225228],
       [ 2.18438808, -0.63015213,  0.7306245 ],
       [ 1.8960649 ,  0.13953076,  0.49930039],
       [ 1.36793674,  1.02409436,  0.37293796],
       [ 1.90386465,  0.00908374,  0.5740634 ],
       [ 1.16277154,  1.05478025,  0.30068793]])

python实现层的类及正向传播

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

代码规范:

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

Sigmoid层

1
2
3
4
5
6
7
8
import numpy as np

class Sigmoid:
def __init__(self):
self.params = [] # 没有学习的参数,使用空列表

def forward(self,x):
return 1 / (1 + np.exp(-x))

Affine层

1
2
3
4
5
6
7
8
9
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网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

应用

1
2
3
4
5
x = np.random.randn(10,2)
model = TwoLayerNet(2,4,3)

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

神经网络的学习

损失函数loss function

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

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

Softmax函数的表达式:

$$y_k = \frac{e{S_{k}}}{\sum_{i=1}n* e^{S_i}}$$

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

交叉熵误差表示为:

$$L=-\sum_kt_klogy_{k}$$

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

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

$$L=-\frac{1}{N}\sum_n\sum_kt_{nk}logy_{nk}$$

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

MatMul层的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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的[…]复制问题

1
2
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
1
id(a),id(b)
(1549258733968, 1549258615664)
1
2
a = b  #  将b赋值给a;
a
array([4, 5, 6])

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

1
id(a),id(b)
(1549258615664, 1549258615664)
1
2
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
1
id(a),id(b)
(1549258733392, 1549258809392)
1
a[...] = b  # 赋值
1
id(a),id(b)
(1549258733392, 1549258809392)

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

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

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

Sigmoid层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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层的正向传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

权重更新

1
2
3
4
5
6
7
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类更新神经网络的参数:

1
2
3
4
5
6
7
8
9
10
# 伪代码

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)
1
2


本文标题:NLP学习1-神经网络的推理

发布时间:2023年01月28日 - 22:01

原始链接:http://www.renpeter.cn/2023/01/28/%E9%B1%BC%E4%B9%A6%E7%AC%94%E8%AE%B01-%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E7%9A%84%E6%8E%A8%E7%90%86.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Coffee or Tea