想要跑程序可以参考这里。关于ndarray和autograd可以参考前面几篇博客。
前言
现在有一个函数,y=w*x+b,w,b已知,那么给一个x,就可以求出对应的一个y。
但当w,b未知时,我们只给出一对x,y,求出的w,b可能只可以满足这一对,但无法满足其他的x,y。这时就需要一个模型来训练出w,b来满足尽可能多的x,y,即给出一定数量的x,y,来推导出符合条件的w,b。
生成数据
现在定义一个函数 y[i] = 2 * x[i][0] -3.4 * x[i][1] + 4.2 + noise(噪音服从均值为0,方差为0.1的正态分布,噪声代表了数据集中无意义的干扰)。这里的w就是[2,-3.4],b就是4.2。或者也可以理解成这个函数有两个w和x,即y=w1*x1+w2*x2+b。
from mxnet import ndarray as nd
from mxnet import autograd
num_inputs = 2 #x有两个维度,相当于有两个输入
num_examples = 1000 #生成1000个数据
true_w = [2,-3.4]
true_b = 4.2
x = nd.random_normal(shape=(num_examples,num_inputs))
#1000行2列的矩阵,每个元素都随机采样于均值为0、标准差为1的正态分布
y = true_w[0] * x[:,0] + true_w[1] * x[:,1] + true_b
#x[:,0]表示每一行的第一列组成的数组
#可以输出x,x[:,0],x[:,0].shape,y.shape看看他们长啥样,也看看他们是啥类型或shape ,刚开始接触可能会有点抽象
y += .01*nd.random_normal(shape=y.shape) #加上一个噪音值noise
#x,y 输出看看随机生成的x矩阵和包含了1000个由x中元素生成的数据的数组
#在这里计算y时我在纠结不同类型和形状的元素计算是否合理,后面我想起了ndarray的广播机制,这也是python带来的便利吧
print(x[0:10],y[0:10]) #矩阵x前十行和由此生成的10个y,根据输出也能看出x和y的shape不同
[[-0.47243103 1.2975348 ]
[ 1.5410181 -2.5207853 ]
[-0.60842186 -1.7573569 ]
[ 0.6143626 0.0028276 ]
[ 0.00257095 -0.5846045 ]
[ 0.64122546 0.0483991 ]
[-0.20711961 -0.34759858]
[ 0.25469646 0.01989137]
[-0.39016405 -2.276683 ]
[-0.5919514 -2.4271743 ]]
<NDArray 10x2 @cpu(0)>
[-1.1587338 15.844224 8.968834 5.409754 6.185884 5.302211
4.9785733 4.640998 11.1503 11.273367 ]
<NDArray 10 @cpu(0)>
读取数据
在训练模型的时候,我们需要遍历数据集并不断读取小批量数据样本。这里我们定义一个函数:它每次返回batch_size(批量大小)个随机样本的特征和标签。
import random
batch_size = 10 #每次读取的数据个数
def data_iter():
idx = list(range(num_examples))
#idx为0—999的list
random.shuffle(idx)
#shuffle()函数将序列的所有元素随机排序
for i in range(0,num_examples,batch_size): #循环100次
j = nd.array(idx[i:min(i+batch_size,num_examples)])
#从idx中取10个元素,个人认为直接写idx[i:i+batch_size]也可以
yield nd.take(x,j),nd.take(y,j)
#nd.take(x,j)相当于取x矩阵中的第j行,nd.take(y,j)相当于对应取y中第y个数据,当然,j是一个数组
#可以print看看nd.take(x,j),nd.take(y,j)是啥
#在 Python 中,使用了 yield 的函数被称为生成器(generator),返回的是一个迭代器,返回的是元组类型。
#这样就有了100组随机数据块,每个数据块包括一个10行2列的矩阵(下面的data)和长度为10的数组 (下面的label)
#第一步中x,y都生成好了,这一步在我看来是为了数据的随机性以及方便数据的读取
让我们读取第一个小批量数据样本并打印。
for data,label in data_iter():
print(data,label) #打印第一个随机数据块
break
[[ 0.7542019 -0.48587778]
[-1.9441624 -0.91037935]
[ 0.13180183 0.88579226]
[-1.4955239 0.737821 ]
[-0.88221204 -0.18438959]
[-0.7792825 -0.53876454]
[-0.8198182 1.4236803 ]
[ 0.02309756 -0.29708868]
[ 0.05650486 -0.6636138 ]
[ 2.4149287 0.48304093]]
<NDArray 10x2 @cpu(0)>
[ 7.357671 3.4120867 1.4567578 -1.3204895 3.057557 4.484297
-2.299931 5.250752 6.5591908 7.3872066]
<NDArray 10 @cpu(0)>
初始化模型函数
目的:已知y和x,要求w和b,那就先取随机的w和b。
w = nd.random_normal(shape=(num_inputs,1)) #w是2行1列的矩阵
b = nd.zeros((1,))
params = [w,b] #params是list
w,b,params #此时的w,b和我们理想的情况差很多,需要训练来接近
(
[[1.201833 ]
[0.29849657]]
<NDArray 2x1 @cpu(0)>,
[0.]
<NDArray 1 @cpu(0)>,
[
[[1.201833 ]
[0.29849657]]
<NDArray 2x1 @cpu(0)>,
[0.]
<NDArray 1 @cpu(0)>])
之后训练时我们需要对这些参数求导来更新它们的值,所以我们需要创建它们的梯度。
for param in params:
param.attach_grad()
定义模型
def net(x): #返回的是我们的预测值yhat(按当前的参数w,b来计算f(x)得到的预计y)
return nd.dot(x,w) + b
#此处x是L行2列的矩阵,w是2行1列的矩阵
#相乘得到L行1列的矩阵,每一行元素就是x[i][0]*w[0]+x[i][1]*w[1]
#矩阵相乘得到L行1列的矩阵再加上b
#这里出现了广播机制(对两个形状不同的NDArray按元素进行运算时)
跑一跑试试。
print(data) #上面生成的data
print(net(data)) #通过模型得到预测值
[[ 0.7542019 -0.48587778]
[-1.9441624 -0.91037935]
[ 0.13180183 0.88579226]
[-1.4955239 0.737821 ]
[-0.88221204 -0.18438959]
[-0.7792825 -0.53876454]
[-0.8198182 1.4236803 ]
[ 0.02309756 -0.29708868]
[ 0.05650486 -0.6636138 ]
[ 2.4149287 0.48304093]]
<NDArray 10x2 @cpu(0)>
[[ 0.7613919 ]
[-2.6083038 ]
[ 0.42280975]
[-1.577133 ]
[-1.1153111 ]
[-1.0973868 ]
[-0.56032085]
[-0.06092054]
[-0.13017704]
[ 3.046527 ]]
<NDArray 10x1 @cpu(0)>
损失函数
def square_loss(yhat,y):
#y原本是数组,此处把y变形成yhat的形状避免自动广播
return(yhat - y.reshape(yhat.shape))**2
#使用平方误差来衡量预测目标和真实目标之间的差距
利用随机梯度下降来求解
我们将参数模型沿着梯度的反方向走特定的距离,这个距离一般叫学习率。
def SGD(params,lr):
for param in params:
param[:] = param-lr*param.grad #这里 - 改 + 会怎么样?
#听说是原地操作,但具体还没有理解透彻,有点像函数的参数传递
#param[:] 不能改成param,这样不会起到学习效果,因为params并没有发生变化,测试如下:
a = nd.array([[1,2],[3,4]])
c = nd.array([1,1])
for b in a:
b = b - c
print(a)
[[1. 2.]
[3. 4.]] #a并没有发生变化
<NDArray 2x2 @cpu(0)>
a = nd.array([[1,2],[3,4]])
c = nd.array([1,1])
for b in a:
b[:] = b - c
print(a)
[[0. 1.]
[2. 3.]]
<NDArray 2x2 @cpu(0)>
训练
迭代数据数次,计算梯度并更新模型参数
epochs = 5 #迭代次数
learning_rate = 0.001 #学习率,可以试试往高了调会怎么样
for e in range(epochs):
total_loss = 0 #损失
for data,label in data_iter(): #取数据,按照上面定义的会取100次
with autograd.record(): #要求导的程序
output = net(data) #通过模型得到output
loss = square_loss(output,label) #真实数据和预测数据的差距
loss.backward() #让loss对w和b求导,要使得loss变小
SGD(params,learning_rate)#修改参数
total_loss += nd.sum(loss).asscalar()
print("Epoch %d ,average loss: %f"%(e,total_loss/num_examples))
Epoch 0 ,average loss: 7.926646
Epoch 1 ,average loss: 0.156229
Epoch 2 ,average loss: 0.003208
Epoch 3 ,average loss: 0.000159
Epoch 4 ,average loss: 0.000097 #损失量(误差)见见减少,如果多迭代几次会发现最终收敛于某一个数
比较一下真实参数和通过学习迭代的得到的参数。
true_w,w
([2, -3.4],
[[ 2.0008717]
[-3.3992171]]
<NDArray 2x1 @cpu(0)>)
true_b,b
(4.2,
[4.199591] #虽有误差但都挺接近的了,因为还有噪音存在
<NDArray 1 @cpu(0)>)
总结
深度学习的第一个程序就花了很多时间来理解,主要一些python的语法还不是很熟悉,开始有一些代码也看不懂,比如take(),索引这类的,一些矩阵、梯度的知识和运算也同之前学过的高数线代联系起来了。李沐大佬的课听了两遍,弄清楚每步的目的和逻辑,也总算是理解了80%,总的来说就是要读取数据,定义模型,训练这三步,程序虽然不长但刚开始就感到了些许困难,希望第一关过了后面会慢慢顺利起来。