目录
1 实现一个简单的线性回归模型
1.1 数据集构建
1.2 模型构建
1.3 损失函数
1.4 模型优化
1.5 模型训练
1.6 模型评估
2 多项式回归
2.1 数据集构建
2.2 模型构建
2.3 模型训练
2.4 模型评估
这里,不知道为什么我的内核经常挂掉,所以在开头附上此代码:
import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE" #防止内核挂掉重启
要通过机器学习来解决一个特定的任务时,我们需要准备5个方面的要素:
首先,我们构造一个小的回归数据集。
def linear_func(x,w=1.2,b=0.5):
y=w*x+b
return y
使用paddle.rand()函数来进行随机采样输入特征x,并代入上面函数得到输出标签。
import paddle
def create_toy_data(func, interval, sample_num, noise = 0.0, add_outlier = False, outlier_ratio = 0.001):
"""
根据给定的函数,生成样本
输入:
- func:函数
- interval: x的取值范围
- sample_num: 样本数目
- noise: 噪声均方差
- add_outlier:是否生成异常值
- outlier_ratio:异常值占比
输出:
- X: 特征数据,shape=[n_samples,1]
- y: 标签数据,shape=[n_samples,1]
"""
# 均匀采样
# 使用paddle.rand生成sample_num个随机数
X = paddle.rand(shape = [sample_num]) * (interval[1]-interval[0]) + interval[0]
y = func(X)
# 生成高斯分布的标签噪声
# 使用paddle.normal生成0均值,noise标准差的数据
epsilon = paddle.normal(0,noise,paddle.to_tensor(y.shape[0]))#生成均值为0,标准差为noise,个数为sample_num的数据
y = y + epsilon
if add_outlier: # 生成额外的异常点
outlier_num = int(len(y)*outlier_ratio)
if outlier_num != 0:
# 使用paddle.randint生成服从均匀分布的、范围在[0, len(y))的随机Tensor
outlier_idx = paddle.randint(len(y),shape = [outlier_num])
y[outlier_idx] = y[outlier_idx] * 5
return X, y
下面生成 150 个带噪音的样本,其中 100 个训练样本,50 个测试样本,并打印出训练数据的可视化分布。
from matplotlib import pyplot as plt # matplotlib 是 Python 的绘图库
func = linear_func
interval = (-10,10)
train_num = 100 # 训练样本数目
test_num = 50 # 测试样本数目
noise = 2 #标准差
#给定函数下生成样本数据
X_train, y_train = create_toy_data(func=func, interval=interval, sample_num=train_num, noise = noise, add_outlier = False)
X_test, y_test = create_toy_data(func=func, interval=interval, sample_num=test_num, noise = noise, add_outlier = False)
X_train_large, y_train_large = create_toy_data(func=func, interval=interval, sample_num=5000, noise = noise, add_outlier = False)
# paddle.linspace返回一个Tensor,Tensor的值为在区间start和stop上均匀间隔的num个值,输出Tensor的长度为num
X_underlying = paddle.linspace(interval[0],interval[1],train_num)
y_underlying = linear_func(X_underlying)
# 绘制数据
plt.scatter(X_train, y_train, marker='*', facecolor="none", edgecolor='#e4007f', s=50, label="train data")
plt.scatter(X_test, y_test, facecolor="none", edgecolor='#f19ec2', s=50, label="test data")
plt.plot(X_underlying, y_underlying, c='#000000', label=r"underlying distribution")#绘制我们的线性函数
plt.legend(fontsize='x-large') # 给图像加图例
# plt.savefig('ml-vis.pdf') # 保存图像到PDF文件中
plt.show()
运行结果如下:
数据已经准备好了。
shape属性是指每个维度上元素的数量,其实无非就是矩阵的运算。
import paddle
from nndl.op import Op
paddle.seed(10) #设置随机种子
# 线性算子
class Linear(Op):
def __init__(self, input_size):
"""
输入:
- input_size:模型要处理的数据特征向量长度
"""
self.input_size = input_size
# 模型参数
self.params = {}
self.params['w'] = paddle.randn(shape=[self.input_size,1],dtype='float32')
self.params['b'] = paddle.zeros(shape=[1],dtype='float32')
def __call__(self, X):
return self.forward(X)
# 前向函数
def forward(self, X):
"""
输入:
- X: tensor, shape=[N,D]
注意这里的X矩阵是由N个x向量的转置拼接成的,与原教材行向量表示方式不一致
输出:
- y_pred: tensor, shape=[N]
"""
N,D = X.shape
if self.input_size==0:
return paddle.full(shape=[N,1], fill_value=self.params['b'])
assert D==self.input_size # 输入数据维度合法性验证
# 使用paddle.matmul计算两个tensor的乘积
y_pred = paddle.matmul(X,self.params['w'])+self.params['b']
return y_pred
# 注意这里我们为了和后面章节统一,这里的X矩阵是由N个x向量的转置拼接成的,与原教材行向量表示方式不一致
input_size = 3
N = 2
X = paddle.randn(shape=[N, input_size],dtype='float32') # 生成2个维度为3的数据
model = Linear(input_size)
y_pred = model(X)
print("y_pred:",y_pred) #输出结果的个数也是2个
运行结果如下:
y_pred: Tensor(shape=[2, 1], dtype=float32, place=Place(cpu), stop_gradient=True,
[[ 0.50950956],
[-2.26490903]])
先说一下,这里需要注意,我这篇博文的程序代码是在下面创建的(内置nndl模块):
如果不是在该文件夹下创建,需要将nndl文件移动到我们创建的文件夹下,否则会出现:
其实 nndl 文件中的op文件也就是上面代码里中间一大段的讲解和实现(就是线性算子)。
在第一节就纳闷怎么突然讲算子,这里就用上了。
算子是构建复杂机器学习模型的基础组件,包含一个函数()的前向函数和反向函数。
算子的接口是这样定义的:
class Op(object):
def __init__(self):
pass
def __call__(self, inputs):
return self.forward(inputs)
# 前向函数
# 输入:张量inputs
# 输出:张量outputs
def forward(self, inputs):
# return outputs
raise NotImplementedError
# 反向函数
# 输入:最终输出对outputs的梯度outputs_grads
# 输出:最终输出对inputs的梯度inputs_grads
def backward(self, outputs_grads):
# return inputs_grads
raise NotImplementedError
这个要对python中的类有一定的基础知识,否则看不懂。
def mean_squared_error(y_true, y_pred):
"""
输入:
- y_true: tensor,样本真实标签
- y_pred: tensor, 样本预测标签
输出:
- error: float,误差值
"""
assert y_true.shape[0] == y_pred.shape[0]
# paddle.square计算输入的平方值
# paddle.mean沿 axis 计算 x 的平均值,默认axis是None,则对输入的全部元素计算平均值。
error = paddle.mean(paddle.square(y_true - y_pred))
return error
# 构造一个简单的样例进行测试:[N,1], N=2
y_true= paddle.to_tensor([[-0.2],[4.9]],dtype='float32')
y_pred = paddle.to_tensor([[1.3],[2.5]],dtype='float32')
error = mean_squared_error(y_true=y_true, y_pred=y_pred).item()#items() 函数以列表返回可遍历的(键, 值) 元组。
print("error:",error)
运行结果如下:
error: 4.005000114440918
def optimizer_lsm(model, X, y, reg_lambda=0):
"""
输入:
- model: 模型
- X: tensor, 特征数据,shape=[N,D]
- y: tensor,标签数据,shape=[N]
- reg_lambda: float, 正则化系数,默认为0
输出:
- model: 优化好的模型
"""
N, D = X.shape
# 对输入特征数据所有特征向量求平均后再转置,为后面求b方便
x_bar_tran = paddle.mean(X,axis=0).T
# 求标签的均值,shape=[1]
y_bar = paddle.mean(y)
# paddle.subtract通过广播的方式实现矩阵减向量
x_sub = paddle.subtract(X,x_bar_tran)
# 使用paddle.all判断输入tensor是否全0
if paddle.all(x_sub==0):
model.params['b'] = y_bar
model.params['w'] = paddle.zeros(shape=[D])
return model
# paddle.inverse求方阵的逆
tmp = paddle.inverse(paddle.matmul(x_sub.T,x_sub)+
reg_lambda*paddle.eye(num_rows = (D)))
w = paddle.matmul(paddle.matmul(tmp,x_sub.T),(y-y_bar))
b = y_bar-paddle.matmul(x_bar_tran,w)
model.params['b'] = b
model.params['w'] = paddle.squeeze(w,axis=-1)
return model
训练样本数目(100)较少时:
input_size = 1
model = Linear(input_size)
model = optimizer_lsm(model,X_train.reshape([-1,1]),y_train.reshape([-1,1]))
print("w_pred:",model.params['w'].item(), "b_pred: ", model.params['b'].item())
y_train_pred = model(X_train.reshape([-1,1])).squeeze()
train_error = mean_squared_error(y_true=y_train, y_pred=y_train_pred).item()
print("train error: ",train_error)
运行结果如下:
w_pred: 1.1364599466323853 b_pred: 0.2283352017402649
train error: 3.3990638256073
训练样本数目(5000)较大时:
model_large = Linear(input_size)
model_large = optimizer_lsm(model_large,X_train_large.reshape([-1,1]),y_train_large.reshape([-1,1]))
print("w_pred large:",model_large.params['w'].item(), "b_pred large: ", model_large.params['b'].item())
y_train_pred_large = model_large(X_train_large.reshape([-1,1])).squeeze()
train_error_large = mean_squared_error(y_true=y_train_large, y_pred=y_train_pred_large).item()
print("train error large: ",train_error_large)
运行结果如下:
w_pred large: 1.197813630104065 b_pred large: 0.5165650248527527
train error large: 4.074573516845703
y_test_pred = model(X_test.reshape([-1,1])).squeeze()
test_error = mean_squared_error(y_true=y_test, y_pred=y_test_pred).item()
print("test error: ",test_error)
运行结果如下:
test error: 3.019052743911743
y_test_pred_large = model_large(X_test.reshape([-1,1])).squeeze()
test_error_large = mean_squared_error(y_true=y_test, y_pred=y_test_pred_large).item()
print("test error large: ",test_error_large)
运行结果如下:
test error large: 2.5873899459838867
import math
# sin函数: sin(2 * pi * x)
def sin(x):
y = paddle.sin(2 * math.pi * x)
return y
构建训练和测试数据,其中训练数样本 15 个,测试样本 10 个,高斯噪声标准差为 0.1,自变量范围为 (0,1)。
# 生成数据
func = sin
interval = (0,1)
train_num = 15
test_num = 10
noise = 0.5 #0.1
X_train, y_train = create_toy_data(func=func, interval=interval, sample_num=train_num, noise = noise)
X_test, y_test = create_toy_data(func=func, interval=interval, sample_num=test_num, noise = noise)
#下图中黑线所示
X_underlying = paddle.linspace(interval[0],interval[1],num=100)
y_underlying = sin(X_underlying)
# 绘制图像
plt.rcParams['figure.figsize'] = (8.0, 6.0)
plt.scatter(X_train, y_train, facecolor="none", edgecolor='#e4007f', s=50, label="train data")
#plt.scatter(X_test, y_test, facecolor="none", edgecolor="r", s=50, label="test data")
plt.plot(X_underlying, y_underlying, c='#000000', label=r"$\sin(2\pi x)$")
plt.legend(fontsize='x-large')
# plt.savefig('ml-vis2.pdf')
plt.show()
运行结果如下:
多项式回归和线性回归一样,同样需要学习参数,只不过需要对输入特征()根据多项式阶数进行变换。因此,我们可以套用求解线性回归参数的方法来求解多项式回归参数。
# 多项式转换
def polynomial_basis_function(x, degree = 2):
"""
输入:
- x: tensor, 输入的数据,shape=[N,1]
- degree: int, 多项式的阶数
example Input: [[2], [3], [4]], degree=2
example Output: [[2^1, 2^2], [3^1, 3^2], [4^1, 4^2]]
注意:本案例中,在degree>=1时不生成全为1的一列数据;degree为0时生成形状与输入相同,全1的Tensor
输出:
- x_result: tensor
"""
if degree==0:
return paddle.ones(shape = x.shape,dtype='float32')
x_tmp = x
x_result = x_tmp
for i in range(2, degree+1):
x_tmp = paddle.multiply(x_tmp,x) # 逐元素相乘算子,输入 x 与输入 y 逐元素相乘,并将各个位置的输出元素保存到返回结果中。因为x_tmp和x是相同的张量,所以逐元素相乘时对应的元素是相同的,即2*2,3*3,也就是平方
x_result = paddle.concat((x_result,x_tmp),axis=-1)#对输入沿 axis 轴进行联结(axis=-1,即按列拼接),返回一个新的Tensor。
return x_result
# 简单测试
data = [[2], [3], [4]]
X = paddle.to_tensor(data = data,dtype='float32')
degree = 3
transformed_X = polynomial_basis_function(X,degree=degree)
print("转换前:",X)
print("阶数为",degree,"转换后:",transformed_X)
运行结果如下:
转换前: Tensor(shape=[3, 1], dtype=float32, place=Place(cpu), stop_gradient=True,
[[2.],
[3.],
[4.]])
阶数为 3 转换后: Tensor(shape=[3, 3], dtype=float32, place=Place(cpu), stop_gradient=True,
[[2. , 4. , 8. ],
[3. , 9. , 27.],
[4. , 16., 64.]])
对于多项式回归,我们可以同样使用前面线性回归中定义的LinearRegression
算子、训练函数train
、均方误差函数mean_squared_error
。拟合训练数据的目标是最小化损失函数,同线性回归一样,也可以通过矩阵运算直接求出的值。
我们设定不同的多项式阶,的取值分别为0、1、3、8,之前构造的训练集上进行训练,观察样本数据对sin曲线的拟合结果。
plt.rcParams['figure.figsize'] = (12.0, 8.0) #图框属性
#enumerate非常好用,有时我们可以更改索引
for i, degree in enumerate([0, 1, 3, 8]): # []中为多项式的阶数,对于这个,i在索引[0,1,2,3]中遍历,degree在[0,1,3,8]中遍历
model = Linear(degree)
#reshape([-1,1])是改变为n行1列,reshape([-1])是改变为1行n列
X_train_transformed = polynomial_basis_function(X_train.reshape([-1,1]), degree)#在保持输入 x 数据不变的情况下,改变 x 的形状。
X_underlying_transformed = polynomial_basis_function(X_underlying.reshape([-1,1]), degree)
model = optimizer_lsm(model,X_train_transformed,y_train.reshape([-1,1])) #拟合得到参数
y_underlying_pred = model(X_underlying_transformed).squeeze()#squeeze()会删除输入Tensor的Shape中尺寸为1的维度(元素个数为1的轴)。如果指定了axis,则会删除axis中指定的尺寸为1的维度。如果没有指定axis,那么所有等于1的维度都会被删除。
print(model.params)
# 绘制图像
plt.subplot(2, 2, i + 1)
plt.scatter(X_train, y_train, facecolor="none", edgecolor='#e4007f', s=50, label="train data")#图中粉色○
plt.plot(X_underlying, y_underlying, c='#000000', label=r"$\sin(2\pi x)$")#图中黑色线
plt.plot(X_underlying, y_underlying_pred, c='#f19ec2', label="predicted function")#图中粉色实线
plt.ylim(-2, 1.5)
plt.annotate("M={}".format(degree), xy=(0.95, -1.4))
#plt.legend(bbox_to_anchor=(1.05, 0.64), loc=2, borderaxespad=0.)
plt.legend(loc='lower left', fontsize='x-large')
# plt.savefig('ml-vis3.pdf')
plt.show()
运行结果如下:
下面通过均方误差来衡量训练误差、测试误差以及在没有噪音的加入下sin
函数值与多项式回归值之间的误差,更加真实地反映拟合结果。多项式分布阶数从0到8进行遍历。
# 训练误差和测试误差
training_errors = []
test_errors = []
distribution_errors = []
# 遍历多项式阶数
for i in range(9):
model = Linear(i)
X_train_transformed = polynomial_basis_function(X_train.reshape([-1,1]), i)
X_test_transformed = polynomial_basis_function(X_test.reshape([-1,1]), i)
X_underlying_transformed = polynomial_basis_function(X_underlying.reshape([-1,1]), i)
optimizer_lsm(model,X_train_transformed,y_train.reshape([-1,1]))
y_train_pred = model(X_train_transformed).squeeze()
y_test_pred = model(X_test_transformed).squeeze()
y_underlying_pred = model(X_underlying_transformed).squeeze()
train_mse = mean_squared_error(y_true=y_train, y_pred=y_train_pred).item()
training_errors.append(train_mse)
test_mse = mean_squared_error(y_true=y_test, y_pred=y_test_pred).item()
test_errors.append(test_mse)
#distribution_mse = mean_squared_error(y_true=y_underlying, y_pred=y_underlying_pred).item()
#distribution_errors.append(distribution_mse)
print ("train errors: \n",training_errors)
print ("test errors: \n",test_errors)
#print ("distribution errors: \n", distribution_errors)
# 绘制图片
plt.rcParams['figure.figsize'] = (8.0, 6.0)
plt.plot(training_errors, '-.', mfc="none", mec='#e4007f', ms=10, c='#e4007f', label="Training")
plt.plot(test_errors, '--', mfc="none", mec='#f19ec2', ms=10, c='#f19ec2', label="Test")
#plt.plot(distribution_errors, '-', mfc="none", mec="#3D3D3F", ms=10, c="#3D3D3F", label="Distribution")
plt.legend(fontsize='x-large')
plt.xlabel("degree")
plt.ylabel("MSE")
# plt.savefig('ml-mse-error.pdf')
plt.show()
运行结果如下:
train errors:
[0.5963446497917175, 0.2896002531051636, 0.26379576325416565, 0.23111073672771454, 0.17822088301181793, 0.1385764479637146, 0.16013973951339722, 0.3500741422176361, 0.19967743754386902]
test errors:
[0.4975295066833496, 0.4701949954032898, 0.38963937759399414, 0.3042631447315216, 0.29470306634902954, 0.4156731069087982, 0.3180873394012451, 0.7467994093894958, 0.2483164519071579]
degree = 8 # 多项式阶数
reg_lambda = 0.0001 # 正则化系数
X_train_transformed = polynomial_basis_function(X_train.reshape([-1,1]), degree)
X_test_transformed = polynomial_basis_function(X_test.reshape([-1,1]), degree)
X_underlying_transformed = polynomial_basis_function(X_underlying.reshape([-1,1]), degree)
model = Linear(degree)
optimizer_lsm(model,X_train_transformed,y_train.reshape([-1,1]))
y_test_pred=model(X_test_transformed).squeeze()
y_underlying_pred=model(X_underlying_transformed).squeeze()
model_reg = Linear(degree)
optimizer_lsm(model_reg,X_train_transformed,y_train.reshape([-1,1]),reg_lambda=reg_lambda)#唯一区别
y_test_pred_reg=model_reg(X_test_transformed).squeeze()
y_underlying_pred_reg=model_reg(X_underlying_transformed).squeeze()
mse = mean_squared_error(y_true = y_test, y_pred = y_test_pred).item()
print("mse:",mse)
mes_reg = mean_squared_error(y_true = y_test, y_pred = y_test_pred_reg).item()
print("mse_with_l2_reg:",mes_reg)
# 绘制图像
plt.scatter(X_train, y_train, facecolor="none", edgecolor="#e4007f", s=50, label="train data")
plt.plot(X_underlying, y_underlying, c='#000000', label=r"$\sin(2\pi x)$")
plt.plot(X_underlying, y_underlying_pred, c='#e4007f', linestyle="--", label="$deg. = 8$")
plt.plot(X_underlying, y_underlying_pred_reg, c='#f19ec2', linestyle="-.", label="$deg. = 8, \ell_2 reg$")
plt.ylim(-1.5, 1.5)
plt.annotate("lambda={}".format(reg_lambda), xy=(0.82, -1.4))
plt.legend(fontsize='large')
# plt.savefig('ml-vis4.pdf')
plt.show()
运行结果如下:
mse: 0.2483164519071579
mse_with_l2_reg: 0.32638686895370483
和之前机器学习实战是有区别的,但是思路是一样的,不过感觉机器学习实战的代码相对简单,因为都是在调用现成的包。