>>> A = np.array([[1,2],[3,4]])
>>> B = np.array([[3,0],[0,6]])
>>> A*B
[[3 0]
[0 24]]
>>> np.multiple(A,B)
[[3 0]
[0 24]]
ndarry的*
乘法实际上指的是两个形状相同的矩阵中对应元素相乘得到的一个乘积矩阵,与np.multiple()
的功能相同。
>>> np.dot(A,B)
[[3 12]
[9 24]]
np.dot()
方法实现了线性代数中的矩阵乘法,后续神经网络中的矩阵相乘用的就是点乘
值得注意的是,因为np中广播特性的存在,可以允许不同形状的矩阵进行*
乘,但要求相乘矩阵其中之一要可以扩展成另一个的形状。例如有A.shape=(m,n) B.shape=(x,y)
,当且仅当x=m,1 y=n,1
时式A*B
才合法。否则,即使m=kx, n=ly
也会产生ValueError。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G4ymhfNO-1661064417354)(C:\Users\LiuYuehan\AppData\Roaming\Typora\typora-user-images\image-20220809173630299.png)]
数组索引
可以直接用np.array进行不等式运算,返回的不是单个布尔值,而是与原矩阵相同形状的一个布尔矩阵。在一个矩阵的索引处直接放入一个相同形状的布尔矩阵,结果为其中布尔值为真的位置的元素。
>>> A>0
[[False True]
[ True True]]
>>> A[A>0]
[2 3 4]
>>> B[A>0]
[0 0 6]
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(0,6,0.1)
y = np.sin(x)
plt.plot(x,y)
plt.show()
上述代码用于输出一个正弦函数的局部图像,但当我第一次运行时报错了:
qt.qpa.plugin: Could not find the Qt platform plugin “windows” in “”
This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem.
显然这是Qt组件的错误,但我知道anaconda自带了pyqt5,且我之前跟着鱼书做的时候没有出现这个问题。所以我立刻就怀疑是因为我装了pyside的缘故,可能是顶掉了pyqt的某些插件,但我没有证据。
于是我分别在百度上搜索了这两个错误信息,得到可能有用的解答是:a. pyqt的路径没有添加到环境变量中;b. pyqt的版本(可能是与matplotlib)不匹配。
事实证明我的情况并不在上述二种之列,添加环境变量毫无用处,重装pyqt也毫无用处(此处需要注意,我使用conda卸载pyqt后其还自动卸载了numpy和matplotlib,尚不知是否还有其它池鱼,不过在我重装pyqt后应该也都回复了吧)。
直到我卸载了pyside6,终于可以正常显示图像了。
1️⃣ 声明变量关系
x = np.arange(0,6,0.1)
y = np.sin(x)
x,y是长度相同的两个一维数组,若数组长度不同,在plot时会报错:
ValueError: x and y must have same first dimension, but have shapes (65,) and (60,)
2️⃣作图
plt.plot(x,y)
plot可以接收两个以上的变量,若是偶数个,会自动两两成对组成(x,y)的形式,同样要求成对的两个数组长度相同;若是奇数个,最后一个变量会作为因变量y与同规模的单位步长数组成对。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r16rGxU1-1661064417355)(C:\Users\LiuYuehan\AppData\Roaming\Typora\typora-user-images\image-20220812094243831.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gyTmEiMB-1661064417355)(C:\Users\LiuYuehan\AppData\Roaming\Typora\typora-user-images\image-20220812094320211.png)]
plot除了能接收变量参数外,还可以接收绘图相关的参数,如
label:曲线标记,使用
plot.legend()
可以在图例中显示linestyle:曲线类型,已知虚线为
linstyle = "--"
3️⃣显示
plt.legend() #图例
plt.title("sin") #标题
plt.ylim(-1,1) #y轴显示范围
plt.show() #打开窗口,显示图像
将输入信号的总和(WX+b)映射为输出信号(y),即 y = h ( W X + b ) y=h(WX+b) y=h(WX+b)
激活函数需要是非线性的,目的是突出多层优势:不论多少层嵌套的线性函数总能被另一个线性函数代替
f u n 1 f 3 ( f 2 ( f 1 ( x ) ) ) = C 3 [ C 2 ( C 1 x + b 1 ) + b 2 ] + b 3 = C 3 C 2 C 1 x + C 3 C 2 b 1 + C 3 b 2 + b 3 = C 4 x + b 4 = f 4 ( x ) {fun1} f_3(f_2(f_1(x)))=C_3[C_2(C_1x+b_1)+b_2]+b_3 \\=C_3C_2C_1x+C_3C_2b1+C_3b_2+b3 \\=C_4x+b_4 \\=f_4(x) fun1f3(f2(f1(x)))=C3[C2(C1x+b1)+b2]+b3=C3C2C1x+C3C2b1+C3b2+b3=C4x+b4=f4(x)
输出层的激活函数一般使用恒等函数(回归问题)、sigmoid函数(二元分类问题)或softmax函数(多元分类问题)
阶跃函数
感知机使用的经典激活函数,非平滑的分段函数,且只能有两个值输出。阶跃函数和ReLU函数都存在导数值为0的部分,这表示使用这两种函数作为输出层激活函数的神经网络在一些情况下会对输入和权重的变化失去反应,即调整网络参数也不会带来损失值的变化,导致网络无法进行学习。
h ( x ) = { 0 , x ≤ 0 1 , x > 0 h(x)=\begin{cases} 0,x\leq0 \\1,x>0\end{cases} h(x)={0,x≤01,x>0
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gZwPDj4J-1661064417355)(C:\Users\LiuYuehan\AppData\Roaming\Typora\typora-user-images\image-20220814160701611.png)]
sigmoid函数
神经网络区别于感知机的重要部分,平滑的非线性函数,返回值有无数种可能
h ( x ) = 1 1 + e − x h(x)=\frac1{1+e^{-x}} h(x)=1+e−x1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-blAVIgDe-1661064417356)(C:\Users\LiuYuehan\AppData\Roaming\Typora\typora-user-images\image-20220813160441502.png)]
ReLU函数
线性整流函数/修正线性单元。
h ( x ) = { x , x > 0 0 , x ≤ 0 h(x)=\begin{cases}x,x>0 \\0,x\leq0\end{cases} h(x)={x,x>00,x≤0
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rYAokD7v-1661064417356)(C:\Users\LiuYuehan\AppData\Roaming\Typora\typora-user-images\image-20220814165653563.png)]
softmax函数
需要注意的是,softmax函数与之前激活函数的形式有很大不同。前述激活函数,单个神经元输出 y k y_k yk只与输入本神经元的加权结果 a k = W k x + b k a_k=W_kx+b_k ak=Wkx+bk有关;而softmax函数的每个输出值 y k y_k yk都与所有节点的输入有关:
y k = e a k ∑ i = 1 n e a i y_k=\frac{e^{a_k}}{\sum_{i=1}^{n}e^{a_i}} yk=∑i=1neaieak
类似于在输出层内部形成了一个全连接层。
根据softmax函数的形式不难看出,softmax函数并不会改变 a a a中元素的大小关系,且有 ∑ i = 1 n y i = 1 \sum_{i=1}^ny_i=1 ∑i=1nyi=1,即所有神经元输出结果的和为1,softmax也是利用此性质来表示多分类任务中不同结果的概率。
而在实际的分类任务中,常常会省略最后输出层的softmax函数,只输出最大的结果即可。
还有一个很神奇的归一化小技巧:
y k = e a k ∑ i = 1 n e a i = C e a k C ∑ i = 1 n e a i = e a k + log C ∑ i = 1 n e a i + log C = e a k + C ′ ∑ i = 1 n e a i + C ′ y_k=\frac{e^{a_k}}{\sum_{i=1}^{n}e^{a_i}}=\frac{Ce^{a_k}}{C\sum_{i=1}^{n}e^{a_i}} \\=\frac{e^{a_k+\text{log}C}}{\sum_{i=1}^{n}e^{a_i+\text{log}C}} \\=\frac{e^{a_k+C'}}{\sum_{i=1}^{n}e^{a_i+C'}} yk=∑i=1neaieak=C∑i=1neaiCeak=∑i=1neai+logCeak+logC=∑i=1neai+C′eak+C′
def softmax(a):
c = np.mean(a) #使用平均值进行归一化减小运算量
exp_a = np.exp(a-c)
sum_exp_a = np.sum(exp_a-c)
return exp_a/sum_exp_a
欲使 a + C ′ a+C' a+C′的绝对值最小,可以选用输入信号 a a a的平均值或最大值的(鱼书所写的是使用最大值,但私以为有误)。
使用训练好的网络(各层权重和偏置)进行前向推理。
批处理指在一趟操作中将多个数据合并起来,相当于在处理过程中增加了一个维度,能同时得到多个处理结果,按照计算机的运算逻辑可以使整个程序更加高效。
批大小(batch size),一趟处理的数据的数量,常作为步长使用循环控制从总数据中读取,如:
x,t = get_data()
batch_size = 100
acc = 0
for i in range(0, len(x), batch_size): #0, 100, 200...
y = predict(x[i:i+batch_size]) #截取总数据中的一段
p = np.argmax(y,axis=1) #多分类任务选取最大输出作为分类结果,此时p为长度为batch_size的一维数组,内容是批内每个数据的预测结果
acc += np.sum((p==t[i:i+batch]).astype(int)) #统计与标签相同的结果数量,astype(int)可以不写
# 上述两行,若在get_data()时使用one_hot表示数据标签,则无需argmax处理,直接比较y==t[i:i+batch]即可
acc /= len(x)
也称训练,是神经网络自动调节各层权重和偏置数值来拟合数据的过程。
鱼书将神经网络的学习分为4步:
STEP1 从训练数据中随机选出mini-batch
STEP2 计算损失值和各个权重参数的梯度
STEP3 沿梯度下降方向微调参数
STEP4 重复123步
损失值表示某个数据的预测结果与实际标签的差异程度,经由损失函数得到。一般来说,损失值越小,预测结果与实际结果的拟合程度越高,网络性能越好。神经网络的学习过程是以所有数据的平均损失值为指标的。
均方误差
E = 1 2 ∑ k ( y k − t k ) 2 E=\frac12\sum_k(y_k-t_k)^2 E=21k∑(yk−tk)2
其中y为预测结果,t为数据标签的one-hot表示。
def mean_square_error(y,t):
return 0.5*np.sum((y-t)**2)
#其中y和t都是一维np数组
#对照公式可以发现:
#(y-t)**2 == (y-t)*(y-t)
#(y-t)**2 != np.dot(y-t,y-t)
交叉熵误差
E = − ∑ k t k ln y k E=-\sum_kt_k\ \text{ln}\ y_k E=−k∑tk ln yk
此处鱼书上将ln写为log,并文字注明log表示以e为底的自然对数,可能是参考了np中的log。
同前,t也是one-hot表示。
由于 ln 0 = − ∞ \text{ln }0=-\infin ln 0=−∞会导致后续计算无法进行,故在代码中应为 y k y_k yk加上一个正的微小量。实际上,当有 y k = 0 y_k=0 yk=0时,若对应 t k = 0 t_k=0 tk=0,则乘积依然为0,若 t k = 1 t_k=1 tk=1则此项乘积为 − ∞ -\infin −∞,最终结果为 + ∞ +\infin +∞,此时实际标签的正确结果对应的预测为0,误差为无限大。
def cross_entropy_error(y,t):
delta = 1e-7
return -np.sum(t*np.log(y+delta))
#不难看出,np中几乎所有操作都可以对数组以广播方式进行,也可以理解为一种批处理
由于当 t k = 0 t_k=0 tk=0对应的求和项也为0,实际上式中的求和运算只会计算 t k = 1 t_k=1 tk=1时的值。所以当t使用非one-hot表示时的运算其实更加简单:
def cross_entropy_error(y,t):
delta = 1e-7
return -np.log(y[t]+delta)
前面说过,神经网络的学习过程其实是以“让所有数据预测结果的平均损失值最小”这一目标为导向的,但当总数量过大时,每趟学习都需要对所有损失值求和再平均,这带来的开销是不可接受的。小批学习以采样统计的思想,每趟在总数据中随机选取一定数量的数据进行学习(即计算平均损失值),这就大大减少了每趟训练的开销。
train_size = x.shape[0] #数据总量
batch_size = 100 #小批的大小
batch_mask = np.random.choice(tran_size, batch_size) #此处生成了一个随机数列作为掩码
x_batch = x[batch_mask] #将掩码作为下标选出训练批
t_batch = t[batch_mask] #对应的正确标记
但采用小批学习的缺点也很明显。首先,变相缩小了数据规模,小批学习在每趟学习之前要先进行随机选择,每趟的随机选取势必会造成总训练数据的重复或遗漏;其次,不同标签数据参与的比例,使用随机函数同样无法保证训练数据分布是均匀的;再次,相邻的两次训练使用的数据不同,可能会使网络中参数的梯度分布发生较大的改变。这都可能导致网络学习到错误的数据特征。
遗漏重复问题可以通过更加合理的选批操作解决:
实际上,一般的做法是先将所有的训练数据随机打乱,然后按指定的批次大小,按序生成mini-batch。这样每个mini-batch均有一个索引号,······,然后用索引号可以遍历所有的mini-batch。遍历一次所有数据,就成为一个epoch。请注意,本节中的mini-batch每次都是随机选择的,所以不一定每个数据都会被看到。——译者注。
鱼书上写出了小批版本的交叉熵误差函数:
#one-hot:
def cross_entropy_error(y,t):
if y.ndim==1: #若y和t是一维数组,即单个数据的损失值
y = y.reshape(1,y.size)
t = t.reshape(1,t.size)
batch_size = y.shape[0]
delta = 1e-7
return -np.sum(t*np.log(y+delta))
#非one-hot
def cross_entropy_error(y,t):
if y.ndim==1:
y = y.reshape(1,y.size)
t = t.reshape(1,t.size)
batch_size = y.shape[0]
delta = 1e-7
return -np.log( y[np.arange(batch_size),t] + delta )
非one-hot的最后一步非常神奇,y
是一个二维数组,np.arange(batch_size)
是一个一维数组。
以batch_size=3,t=1
为例:y[[0,1,2],1] = [y[0,1],y[1,1],y[2,1]]
。可以看出,下标行位置的数组扩张了结果。考虑如下情况:
y = np.array([[1,2,3],
[4,5,6],
[7,8,9]])
print(y[1,1])
>>>5
############ 一维数组 #############
x = [0,1,2]
print(y[x,1])
>>>[2 5 8]
print(y[1,x])
>>>[4 5 6]
print(y[x,x])
>>>[1 5 9]
############ 两个形状相同的一维数组 #############
z = [2,1,0]
print(y[x,z])
>>>[3 5 7]
############ 两个形状不同的一维数组 #############
w = [0,1]
print(y[x,w])
>>>Traceback (most recent call last):
File "d:\Projects\test.py", line 40, in <module>
print(y[x,w])
IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (3,) (2,)
############ 二维数组和一维数组 #############
v = [[0],[1]]
print(y[v,1])
>>>[[2]
[5]]
print(y[v,x])
>>>[[1 2 3]
[4 5 6]]
print(y[x,v])
>>>[[1 4 7]
[2 5 8]]
############ 二维数组和一维数组 #############
s = [[0,0],[1,1]]
print(y[s,1])
>>>[[2 2]
[5 5]]
print(y[s,x])
>>>Traceback (most recent call last):
File "d:\Projects\test.py", line 52, in <module>
print(y[s,x])
IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (2,2) (3,)
############ 三维数组 #############
t = [[[0,0,0],[0,0,0],[0,0,0]],
[[0,0,0],[0,0,0],[0,0,0]],
[[0,0,0],[0,0,0],[0,0,0]]]
print(y[t,1])
>>>[[[2 2 2]
[2 2 2]
[2 2 2]]
[[2 2 2]
[2 2 2]
[2 2 2]]
[[2 2 2]
[2 2 2]
[2 2 2]]]
print(y[t,x])
>>>[[[1 2 3]
[1 2 3]
[1 2 3]]
[[1 2 3]
[1 2 3]
[1 2 3]]
[[1 2 3]
[1 2 3]
[1 2 3]]]
可以得出结论,二维数组的下标可以接受两个数组作为参数传入,有类似np数组的广播机制。
梯度与方向导数
方向导数是多元函数在某一点沿某直线方向的变化率,如二元函数在某点对x的偏导数,其实就是函数在该点对(0,1)方向(即x轴正方向)的方向导数。
方向导数是一个标量,表示函数在某点某方向上的变化快慢,与导数类似的,正增负减,绝对值越大速度越快。
∂ f ∂ l ∣ ( x 0 , y 0 ) = f x ′ ( x 0 , y 0 ) c o s α + f y ′ ( x 0 , y 0 ) c o s β \frac{\partial f}{\partial l}\Bigg|_{(x_0,y_0)}=f'_x(x_0,y_0)cos\alpha+f'_y(x_0,y_0)cos\beta ∂l∂f∣ ∣(x0,y0)=fx′(x0,y0)cosα+fy′(x0,y0)cosβ
梯度是一个向量,梯度的模是最大的方向导数,方向与该方向导数中的直线方向一致。
grad u ∣ p 0 = ( f x ′ ( x 0 , y 0 ) , f y ′ ( x 0 , y 0 ) ) grad u ( x , y ) = ∂ u ∂ x i + ∂ u ∂ y j \text{grad}u|_{p_0}=(f'_x(x_0,y_0),f'_y(x_0,y_0)) \\\text{grad}u(x,y)=\frac{\partial u}{\partial x}\boldsymbol i+\frac{\partial u}{\partial y}\boldsymbol j gradu∣p0=(fx′(x0,y0),fy′(x0,y0))gradu(x,y)=∂x∂ui+∂y∂uj
梯度方向是可微函数变化率最大的方向,即函数值增加最快的方向,而其反方向就是函数值减少最快的方向
在神经网络中我们的目标函数时损失值函数 L ( Y ) L(Y) L(Y),将需要调节的参数作为自变量,即 L ( Y ) = f ( W , b ) L(Y)=f(W,b) L(Y)=f(W,b)。其中W表示每一层的权重矩阵中所有权重的集合,b表示每一层的偏置的集合,为方便表示,我们将这些参数拉平,函数可表示为 f ( W , b ) = F ( a 1 , a 2 , a 3 , . . . , a n ) f(W,b)=F(a_1,a_2,a_3,...,a_n) f(W,b)=F(a1,a2,a3,...,an)。在复杂的神经网络中,这个函数的参数数量是惊人的。
神经网络的梯度
鱼书中的写法有一些问题,但也有很漂亮的地方:
def numerical_gradient(f,x): #参数f是一个函数,要求其参数列表能够接受x传入
h = 1e-4 #微小量,用于计算数值微分
grad = np.zeros_like(x) #初始化一个0数组,用于储存并返回最终结果
for idx in range(x.size): #此处idx取值范围为x的元素数
tmp_val = x[idx] ####当x维度大于1时,x[idx]表示的依旧是一个数组而不是数,且此时idx可取值范围小于x.size,后续循环会造成数组越界
x[idx] = tmp_val-h
fxh1 = f(x)
x[idx] = tmp_val+h
fxh2 = f(x)
grad[idx] = (fxh1-fxh2)/(2*h) #grad数组保存x中每个元素对应的偏导数,所以本方法中的x[idx]应为每个数的单独操作
x[idx] = tmp_val
return grad
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) #在预测函数中不需要使用输出层的激活函数,而在计算损失值的时候需要加上激活函数
return cross_entropy_error(y,t)
net = simpleNet()
f = lambda W: net.loss(x,t)
dW = numerical_gradient(f,net.W)
最后写法极其玄妙。本来我们要设一个loss=f(W)的函数关系,这个关系可以由loss(x,t)函数反推,但较为复杂,尤其是在网络层数较多时。此处将f设为一个参数为W的函数,内容却直接使用了net的loss函数,没有显式地出现W。
但根据numerical_gradient(f,x)内容可知,只要在更新x的某个值后会带来f(x)值的变化,就可计算对应梯度。那么,只要net.W的变化在numerical_gradient中可以改变f(W)即可。
问题来了,真的可以吗?
按照通常的函数思想,函数内参与运算的应该是形参,其改变不会导致实参改变。那么函数内W的改变应当不会影响net对象的W属性,net执行loss方法的结果也应当不变。但在实际执行过程中,net.W把在函数中改变的形参值带回了实参本体,使得f调用net.loss的结果也发生了改变
原因在于python语言在此处与经典OOL存在差异。python方法的参数传递分为值传递和引用传递两种,其中值传递只改变形参不影响实参,而引用传递会影响实参。要区分这两种参数传递方式,首先要理解python中的可变对象和不可变对象:
常用数据类型:
可变对象:列表list、字典dict、ndarray以及自定义类对象
不可变对象:int、float、string、tuple
作为函数传入参数时,可变对象使用引用传递,不可变对象使用值传递。注意,此处指的是传入参数的类型。例如,如果传入的是obj.a,其类型为属性a的类型,而不是自定义类Obj。
除了函数传参外,可变对象和不可变对象的差异还有许多,如:
def change1(x):
x+=1
def change2(x):
x=x+1
def test(f,x):
print("调用前",x)
f(x)
print("调用后",x)
x1=np.array([1,2,3])
x2=np.array([1,2,3])
print(1)
test(change1,x1)
print(2)
test(change2,x2)
=====================output=======================
1
调用前 [1 2 3]
调用后 [2 3 4]
2
调用前 [1 2 3]
调用后 [1 2 3]
上述代码表明在使用自加运算+=时,python会把结果放在原来的内存中,如果此时是可变对象在函数中的运算,就可以将结果带回本体。而如果是先运算再赋值的话,python会申请新的内存区域存放,此时原内存中的内容不变。关于这点,还有一种更有意思的情况:
x1=np.array([1,2,3])
x2=x1
x1+=1
print(x1,x2)
=====================output=======================
[2 3 4] [2 3 4]
x1=np.array([1,2,3])
x2=x1
x2+=1
print(x1,x2)
=====================output=======================
[2 3 4] [2 3 4]
x1=np.array([1,2,3])
x2=x1
x1=x1+1
print(x1,x2)
=====================output=======================
[2 3 4] [1 2 3]
x1=np.array([1,2,3])
x2=x1+1-1
x1+=1
print(x1,x2)
=====================output=======================
[2 3 4] [1 2 3]
x1=np.array([1,2,3])
print(1,id(x1))
x2=np.array([0,0,0])
print(2,id(x2))
x2=x1
print(2,id(x2))
x1+=1
print(x1,x2)
=====================output=======================
1 2115600393968
2 2115600485616
2 2115600393968
[2 3 4] [2 3 4]
上述代码表明,如果直接用一个变量给另一个变量赋值,python会放弃被赋值变量的原始地址(如果有的话),直接将其指向赋值变量的地址。但若赋值过程中有运算,则会重新申请内存。值得注意的是,不可变对象也是如此情况:
x1=20
print(1,id(x1))
x2=21
print(2,id(x2))
x2=x1
print(2,id(x2))
x1+=1
print(1,id(x1))
print(2,id(x2))
print(x1,x2)
=====================output=======================
1 2129102269328
2 2129102269360
2 2129102269328
1 2129102269360
2 2129102269328
21 20
可以看到在赋值x2=x1时,x2的地址变为与x1相同。