第一次线性回归的训练

  想要跑程序可以参考这里。关于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%,总的来说就是要读取数据,定义模型,训练这三步,程序虽然不长但刚开始就感到了些许困难,希望第一关过了后面会慢慢顺利起来。

上一篇:DeepLearning第一天


下一篇:随机数random