使用机器学习实现奇偶分类

由于后续主要使用机器学习进行流量识别、指纹识别,因此这里主要聚焦于分类问题。

假定背景:对任意给定的数根据奇偶性进行分类。

步骤

大部分情况下,使用机器学习进行分类工作,主要包含以下步骤:

  1. 收集数据
  2. 读入数据
  3. 预处理数据(清除无效数据、数据类型调整、选取特征值)
  4. 将数据分为训练集和测试集
  5. 使用特定参数的模型对训练集进行训练
  6. 使用测试集对训练好的模型进行测试
  7. 调整特征、参数,重复上述工作

实验过程

第一次实验

由于这里只是对整数进行奇偶性判断,因此数据使用生成的随机数即可

import pandas as pd
import random

def g():
    i = random.randint(0,999999)
    return [i, i%2]

data = pd.DataFrame([g() for i in range(100000)])

上述代码将会生成长度为 100000 的列表,并且每个元素都包含随机数与其奇偶性(0 为偶数,1 为奇数)
列表将会被转换成 pandas 的DataFrame类型,以便于后续处理

在 Jupyter Notebook 中,可以使用 data.head() 对数据进行预览(如果不填写参数,默认只显示前 5 个)

0 1
0 319255 1
1 430655 1
2 286442 0
3 709373 1
4 589398 0

这里不存在缺失值,因此暂时不做其他额外处理,只将特征和标签分离即可。

对于pandas.DataFrame,只需要使用中括号即可直接取一列数据。在机器学习中,特征使用X(大写)表示,每一行是一组数据,可能包含多列特征值;标签使用y表示,行数和特征行数相同,表示该数据对应的类型。

在这里,特征和标签都只有一列,分别使用xy表示。由于前面提到,特征往往存在多列,因此后续模型遍历也会认为 x 是一个二维数组。对于一列的数据,在后续操作中无法直接对其训练。在这里使用 numpy 的reshape()来将一维数组转换为二维数组,以便于后续操作。

x = data[0].to_numpy().reshape(-1, 1)
y = data[1]

如果使用 Jupyter Notebook,可以使用单独的两个块输出下xy的值进行再次确认。

接下来则是将数据分为训练集和测试集,使用 sklearn 的train_test_split可以按照要求分割训练集和测试集。通过设置test_size=0.3来将 30% 的数据用于测试,70% 的数据用于训练。同时这里的分割将会乱序分割,以避免数据顺序导致模型训练出现偏差。

from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(x,y, test_size=0.3)

模型拟选用 K-近邻随机森林 两种。这里仅使用相应算法,不需要考虑具体实现,当作黑盒使用即可。

首先是 K-近邻 分类器(KNeighborsClassifier)

from sklearn.neighbors import KNeighborsClassifier

knn = KNeighborsClassifier()
knn.fit(x_train, y_train)
knn.score(x_test, y_test)

接下来是 随机森林 分类器(RandomForestClassifier)

from sklearn.ensemble import RandomForestClassifier
rfc = RandomForestClassifier()                     
rfc.fit(x_train,y_train)                     
rfc.score(x_test,y_test) 

可以发现两个算法只有大概 50% 准确率,约等于瞎猜。

以 K-近邻 为例,其算法实际上基于特征之间的距离。将多维度特征映射至高维空间,将距离较近的数据分类到一起。而显然对于一维数轴,奇偶性和距离没有任何关系。因此对于这些训练器几乎无法得到一个较好的结果。

那么如何对其进行分类呢?

第二次实验

如果采用人脑思维进行奇偶性判断,我们实际上只需要注重于最后一位的奇偶性,或者说二进制下的最后一位就是奇偶性。那么在特征提取中,应该尝试将每位的数提取出来作为特征。而非整体作为特征。

def g():
    i = random.randint(0,999999)
    return list(map(int, list("%010d"%i))) + [i%2]

对上面的g()进行修改,先使用格式化输出将高位用 0 补全到 10 位(必须保证所有数特征个数相同),然后将其分割成单个数字并整合为整数特征。

如下,是新的数据集(最后一列为标签)

0 1 2 3 4 5 6 7 8 9 10
0 0 0 0 0 8 5 1 1 9 1 1
1 0 0 0 0 8 5 3 9 1 1 1
2 0 0 0 0 6 0 9 2 6 0 0
3 0 0 0 0 4 7 8 7 2 8 0
4 0 0 0 0 0 0 8 1 2 5 1

由于这次特征不是单维,因此只需要将前 10 列提取即可

x = data.iloc[:,:10]
y = data[10]

后续代码无需更改,再次执行,即可发现 K-近邻 准确率达到 75% 左右,随机森林 准确率达到 99%。

那么这就完成了么?从原理上来说,K-近邻 在解决该问题上确实存在缺陷,但是准确率达到 99% 的随机森林应该是可以做到 100% 的。

尝试修改模型参数

rfc = RandomForestClassifier(
    n_estimators=81,
    max_features=6, 
    oob_score=True,
    random_state=10,
)

再次执行即可得到 100% 的准确率。

简单总结

对于更为复杂的数据和场景,将会需要更为精妙的方式来提取特征,甚至需要对特征的不同项进行加减操作。同时对于某些模型,要求对数据进行归一化处理(数据除以该列最大值,得到一个 0~1 的浮点数)

看似机器学习可以是由机器进行分类,但实际上除去打标签部分需要手动操作,如何提取特征也需要人进行参与。对特征进行提取需要需要对数据具有深刻理解,甚至在某些情况下与自己手动分类所花费的工作量差别并不太大。

完整代码

# To add a new cell, type '# %%'
# To add a new markdown cell, type '# %% [markdown]'

# %% [markdown]
# # 奇偶性判断
#
# 使用随机数生成数据,并判断数据奇偶性
# %% [markdown]
# ## 读入数据
#
# %%
import pandas as pd
import random

def g():
    i = random.randint(0,999999)
    # return [i, i%2]
    return list(map(int, list("%010d"%i))) + [i%2]

data = pd.DataFrame([g() for i in range(100000)])


# %%
data.head()

# %% [markdown]
# ## 预处理数据
#
# 将特征和标签分离
# %%
# x = data[0].to_numpy().reshape(-1, 1)
# y = data[1]
x = data.iloc[:,:10]
y = data[10]


# %%
x

# %% [markdown]
# ## 分离训练集和测试集
#
# 按照 7:3 的比例分割

# %%
from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(x,y, test_size=0.3)

print(x_train)

# %% [markdown]
# ## 训练并测试模型
#
# 分别使用 K-近邻 和 随机森林 算法
# %%
from sklearn.neighbors import KNeighborsClassifier

knn = KNeighborsClassifier()
knn.fit(x_train, y_train)
knn.score(x_test, y_test)


# %%
from sklearn.ensemble import RandomForestClassifier
rfc = RandomForestClassifier(n_estimators=81,max_features=6, oob_score=True,random_state=10)                     
rfc.fit(x_train,y_train)                     
rfc.score(x_test,y_test)