Press "Enter" to skip to content

手撸机器学习算法 – 非线性问题

 

算法介绍

 

前面两篇分别介绍了 分类 与 回归 问题中各自最简单的算法,有一点相同的是它们都是线性的,而实际工作中遇到的基本都是非线性问题,而能够处理非线性问题是机器学习有实用价值的基础;

 

首先, 非线性问题 在分类与回归中的表现是不同的,在回归问题中,通常指的是无法通过线性模型很好的拟合,而在分类问题中,非线性问题指的是无法通过超平面进行正确的分类;

 

对于非线性问题的处理方法:

 

 

    1. 从 数据 出发:由于非线性是基于当前的特征空间,因此一般可以通过特征转换、升维等方式使得问题在新的特征空间中转为线性(可以推导只要维度足够多,数据总是线性的);

 

    1. 从 模型 出发:使用能处理非线性的模型来处理问题,比如决策树、神经网络等;

 

 

本篇主要从 数据 或者说 特征 的角度来看如何处理分类和回归的非线性问题,这一类处理手段与具体的算法无关,因此有更大的普适性,在机器学习中也被广泛的使用;

 

PS:注意代码中用到的线性回归、感知机等模型都是自己实现的哈,不是sklearn的,所以可能参数、用法、结果并不完全一致;

 

非线性回归问题

 

典型的非线性回归问题数据分布情况如下图,注意 x 为输入特征, y 为输出目标:

可以看到,该数据集无法用一条直线来较好的拟合,而应该使用一条曲线,那幺问题就变成了如何使得只能拟合直线的 线性回归 能够拟合出一条合适的曲线;

 

解决思路

 

由于线性回归只能拟合直线,而当前数据集可视化后明显不是直线,因此解决思路只能从数据上入手,即通过修改坐标系(或者叫特征转换)来改变数据的分布排列情况,使得其更接近于线性;

针对此处的数据集,通过观察其分布情况,考虑将
\(x\)

转换为

 

\(x^2\)

 

,当然,实际工作中不仅需要进行数据探索,同时也需要大量的尝试才能找到最合适的特征转换方法;

 

代码实现

 

构建非线性数据集

 

X = np.array([-1+(1-(-1))*(i/10) for i in range(10)]).reshape(-1,1)
y = (X**2)+rnd.normal(scale=.1,size=X.shape)

 

直接跑线性回归模型

 

model = LR(X=X,y=y)
w,b = model.train()
print(w,b)

 

 

通过特征转换来改变数据分布情况

 

X2 = X**2

 

 

在转换后的数据集上运行线性回归

 

model = LR(X=X2,y=y)
w,b = model.train()
print(w,b)

 

 

在原始坐标系下绘制线性回归的拟合线

 

x_min,x_max = min(X[:,0]),max(X[:,0])
line_x = [x_min+(x_max-x_min)*(i/100) for i in range(100)]
line_y = [model.predict(np.array([x**2])) for x in line_x]

 

 

完整代码

 

import numpy as np
import matplotlib.pyplot as plt
from 线性回归最小二乘法矩阵实现 import LinearRegression as LR
plt.figure(figsize=(18,4))
def pain(pos=141,xlabel='x',ylabel='y',title='',x=[],y=[],line_x=[],line_y=[]):
    plt.subplot(pos)
    plt.title(title)
    plt.xlabel(xlabel)
    plt.ylabel(ylabel)
    plt.scatter(x,y)
    plt.plot(line_x,line_y)
rnd = np.random.RandomState(3)  # 为了演示,采用固定的随机
X = np.array([-1+(1-(-1))*(i/10) for i in range(10)]).reshape(-1,1)
y = (X**2)+rnd.normal(scale=.1,size=X.shape)
model = LR(X=X,y=y)
w,b = model.train()
print(w,b)
line_x = [min(X[:,0]),max(X[:,0])]
line_y = [model.predict(np.array([min(X[:,0])])),model.predict(np.array([max(X[:,0])]))]
pain(131,'x','y','degress=1',X[:,0],y[:,0],line_x,line_y)
X2 = X**2
model = LR(X=X2,y=y)
w,b = model.train()
print(w,b)
line_x = [min(X2[:,0]),max(X2[:,0])]
line_y = [model.predict(np.array([x**2])) for x in line_x]
pain(132,'x^2','y','translate coord & degress=2',X2[:,0],y[:,0],line_x,line_y)
x_min,x_max = min(X[:,0]),max(X[:,0])
line_x = [x_min+(x_max-x_min)*(i/100) for i in range(100)]
line_y = [model.predict(np.array([x**2])) for x in line_x]
pain(133,'x','y','degress=2',X[:,0],y[:,0],line_x,line_y)
plt.show()

 

非线性分类问题

 

线性不可分的情况在分类问题中比比皆是,简单的不可分情况如下:

虽然问题类型不同,但是我们的解决思路是一致的,即通过特征转换将线性不可分问题转为线性可分;

 

针对上述数据分布,可以观察到不同类型的点距离中心点的距离差异很明显,圆点距离中心的距离很近,而叉叉距离中心的距离很远,如果能够构建一个表示该距离的特征,那幺就可以基于该特征进行分类;

基于上述分析,通过
欧氏距离 公式计算点到原点的距离有:
\(\sqrt{(x1-0)^2+(x2-0)^2}\) ,由于我们只是期望获得一个广义上的距离,并不严格要求是欧氏距离,因此将公式中的根号去掉也不影响对数据分布的改变,最后特征转换为将
\(x_1\) 转换为
\(x_1^2\) ,同样的将
\(x_2\)

转换为

 

\(x_2^2\)

 

,转换后的横纵坐标值之和就可以用于表示我们期望的距离;

 

代码实现

 

构建线性不可分数据集

 

X = np.array([[-1.8,0.6],[0.48,-1.36],[3.68,-3.64],[1.44,0.52],[3.42,3.5],[-4.18,1.68]])
y = np.array([1,1,-1,1,-1,-1])

 

直接运行感知机模型

 

model = Perceptron(X=X,y=y,epochs=100)
w,b = model.train()

 

 

特征转换及转换后的特征分布

 

Z = X**2 # z1=x1^2,z2=x2^2

 

 

在转换后的数据集上运行感知机

 

model = Perceptron(X=Z,y=y,epochs=100)
w,b = model.train()

 

 

在原始坐标系下看感知机拟合的超平面

 

line_x = [(line_x[0]+(line_x[1]-line_x[0])*(i/100))**.5 for i in range(0,100)]
line_y = [((-b-w[0]*(x**2))/w[1])**.5 for x in line_x]

 

 

通过增加任意二阶以及小于二阶特征来拟合任意二次曲线

 

Z2 = np.array([[x[0]**2,x[1]**2,x[0]*x[1],x[0],x[1]] for x in X])
model = Perceptron(X=Z2,y=y,w=np.array([0,0,0,0,0]),epochs=100)
w,b = model.train()

 

 

完整代码

 

import numpy as np
import matplotlib.pyplot as plt
from 感知机口袋算法 import Perceptron
plt.figure(figsize=(18,6))
'''
通过坐标转换/特征转换将非线性问题转为线性问题,再使用线性模型解决;
'''
def trans_z(X):
    return X**2
def trans_z2(X):
    return np.array([[x[0]**2,x[1]**2,x[0]*x[1],x[0],x[1]] for x in X])
def pain(pos=121,title='',xlabel='',ylabel='',resolution=0.05,model=None,X=[],y=[],line_x=[],line_y=[],transform=None):
    plt.subplot(pos)
    plt.title(title)
    plt.xlabel(xlabel)
    plt.ylabel(ylabel)
    xy_min = min(min([x[0] for x in X]),min([x[1] for x in X]))
    xy_max = max(max([x[0] for x in X]),max([x[1] for x in X]))
    xx1, xx2 = np.mgrid[xy_min-1:xy_max+1.1:resolution, xy_min-1:xy_max+1.1:resolution]
    grid = np.c_[xx1.ravel(), xx2.ravel()]
    if transform:
        grid = transform(grid)
    y_pred = np.array([model.predict(np.array(x)) for x in grid]).reshape(xx1.shape)
    plt.contourf(xx1, xx2, y_pred, 25, cmap="coolwarm", vmin=0, vmax=1, alpha=0.8)
    plt.scatter([xi[0] for xi,yi in zip(X,y) if yi==1],[xi[1] for xi,yi in zip(X,y) if yi==1],c='black',marker='o')
    plt.scatter([xi[0] for xi,yi in zip(X,y) if yi==-1],[xi[1] for xi,yi in zip(X,y) if yi==-1],c='black',marker='x')
    # plt.plot(line_x,line_y,color='black')
## 不可分
X = np.array([[-1.8,0.6],[0.48,-1.36],[3.68,-3.64],[1.44,0.52],[3.42,3.5],[-4.18,1.68]])
y = np.array([1,1,-1,1,-1,-1])
model = Perceptron(X=X,y=y,epochs=100)
w,b = model.train()
# 注意绘制分割直线公式为:wx+b=0,因此给定x[0],计算对应的x[1]即可画图
# w[0]*x[0]+w[1]*x[1]+b=0 => x[1]=(-b-w[0]*x[0])/w[1]
line_x = [min([x[0] for x in X])-3,max([x[0] for x in X])+3]
line_y = [(-b-w[0]*line_x[0])/w[1],(-b-w[0]*line_x[1])/w[1]]
pain(141,'Before coordinate translate','x1','x2',model=model,X=X,y=y)
## 转换坐标为可分
Z = X**2 # z1=x1^2,z2=x2^2,相当于对原数据空间做坐标系转换,也可以理解为特征转换
model = Perceptron(X=Z,y=y,epochs=100)
w,b = model.train()
line_x = [min([x[0] for x in Z])-3,max([x[0] for x in Z])+3]
line_y = [(-b-w[0]*line_x[0])/w[1],(-b-w[0]*line_x[1])/w[1]]
pain(142,'After coordinate translate','z1=x1^2','z2=x2^2',model=model,X=Z,y=y)
## 转换回原坐标绘制分割线,此时为曲线
line_x = [(line_x[0]+(line_x[1]-line_x[0])*(i/100))**.5 for i in range(0,100)]
line_y = [((-b-w[0]*(x**2))/w[1])**.5 for x in line_x]
pain(143,'Back to original coordinate','x1','x2',model=model,X=X,y=y,transform=trans_z)
## 使用任意二次曲线转换坐标:所有可能的二元二次方程
Z2 = np.array([[x[0]**2,x[1]**2,x[0]*x[1],x[0],x[1]] for x in X])
model = Perceptron(X=Z2,y=y,w=np.array([0,0,0,0,0]),epochs=100)
w,b = model.train()
# w0*x0^2+w1*x1^2+w2*x0*x1+w3*x0+w4*x1+b=0 => x1=-b-w3*x0-w0*x0^2
# line_x = [(line_x[0]+(line_x[1]-line_x[0])*(i/100))**.5 for i in range(0,100)]
# line_y = [((-b-w[0]*(x**2))/w[1])**.5 for x in line_x]
pain(144,'Back to original coordinate','x1','x2',model=model,X=X,y=y,transform=trans_z2)
plt.show()

 

最后

 

对于特征转换,可以应用的方法很多,本篇主要是以最简单的二次多项式进行转换,实际上对于更复杂的数据,需要进行更高阶的转换,当然也可以基于业务进行特征转换等等,通常这也是ML中非常消耗时间成本的一个步骤,也是对于最终结果影响最大的一步;

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注