
机器学习是一种从数据中总结规律的统计方法。机器学习中有各种用于总结规律并进行预测或者分类的模型(算法),被广泛应用在手写文字识别、物体识别、文本分类、语音识别、股价预测和疾病诊断等领域(图1)。因惊人的图像识别精度而爆红的深度学习也是机器学习的一部分(图2)。深度学习是神经网络模型的一种形式,模拟了人脑中神经细胞的活动。

图1

图2
如今我们迎来了一个非常美好的世界:汇集了包括深度学习在内的各种机器学习模型的库不断问世,并且向所有人免费公开。通过这些库,我们可以轻松地制作出十分厉害的软件。即使不理解模型中的计算原理,我们也可以大胆尝试,如果可以得到预期的结果,就有可能制作出有用的东西。
话虽如此,但肯定也有人希望充分理解机器学习的原理和理论。首先,了解原理本身就是一件令人兴奋的事情。其次,掌握了原理,在面对问题时就可以选择更加合适的模型,在运行结果不理想时也能找到更加合适的对策。更厉害的是,我们甚至能独自开发出符合自身目的的独创模型。
机器学习分类
机器学习中的问题大致可以分为三种,分别是有监督学习的问题、无监督学习的问题和强化学习的问题。有监督学习要求对于输入给出相应的输出;无监督学习要求发现输入数据的规律;强化学习则要求像国际象棋那样,找出使最后结果(准确地说是整体的结果)达到最优的动作。
什么是无监督学习
现实生活中常常会有这样的问题:缺乏足够的先验知识,因此难以人工标注类别或进行人工类别标注的成本太高。很自然地,我们希望计算机能代我们完成这些工作,或至少提供一些帮助。根据类别未知(没有被标记)的训练样本解决模式识别中的各种问题,称之为无监督学习。无监督学习包括聚类、降维和异常检测等,今天我们来一起看一下聚类。
二维输入数据
聚类就是在不使用类别信息的前提下,把输入数据中相似的数据分成不同的类别的操作。
图 3 展示了二维数据 X 的分布,但是没有根据信息以颜色区分。即使不用颜色区分,但仔细观察,也依然能看出数据分布有一定规律:上方(x0 = 0.5、x1 = 1 附近) 和 右下方(x0 = 1、x1 = -0.5 附近) 数据各成一块;左下方的数据点散布在广大范围内,这个区域或许也可以看作一个大数据块。
这样的数据分布的块称为簇(cluster)。从数据分布中找到簇,将属于同一个簇的数据点分配到同一个类别(标签),将属于其他簇的数据点分配到另一个类别的操作就是聚类。

图3
那聚类有什么用呢?属于同一个簇的数据点可以看作“相似的”,属于不同簇的数据点可以看作“不相似的”。
如果能对顾客数据(消费金额及购物时间段等)进行聚类,那么输出的类别将是家庭主妇或者上班族等,顾客将被表示为不同的类别,这样就可以针对不同的类别实施不同的销售策略。
再看昆虫的例子,如果采用的昆虫数据(体重、身长及头部大小等)中有两个簇,也许就能从数据中发现昆虫存在两个亚种。
聚类算法有很多种,本次将介绍最常用的 K-means 算法。
K-means 算法的概要
下面依次说明这个算法的步骤,详见图4。

图4
对于 K-means 算法,我们需要事先决定要分割的簇数 K。在本例中,我们设 K = 3,即分为 3 个簇。
K-means 算法使用 2 个变量:表示簇的中心位置的中心向量 μ 和表示各数据点属于哪个簇的类别指示变量 R。
在步骤 0 中,随意赋予簇的中心向量 μ 一个初始值,这样就暂时确定了簇的中心。在步骤 1 中,根据当前的簇的中心向量 μ 确定类别指示变量R。
在步骤 2 中,根据当前的类别指示变量 R 更新 μ。
然后重复步骤 1 和步骤 2,不断地更新 μ 和 R,直到二者的值不再发生变化。
下面就让我们详细看一下每个步骤。
步骤 0:准备变量与初始化
首先创建在图形上显示输入数据 X 与 Mu、R 的函数。
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# 生成数据 --------------------------------
np.random.seed(1)
N = 100
K = 3
T3 = np.zeros((N, 3), dtype=np.uint8)
X = np.zeros((N, 2))
X_range0 = [-3, 3]
X_range1 = [-3, 3]
X_col = ['cornflowerblue', 'black', 'white']
Mu = np.array([[-.5, -.5], [.5, 1.0], [1, -.5]]) # 分布的中心
Sig = np.array([[.7, .7], [.8, .3], [.3, .8]]) # 分布的离散值
Pi = np.array([0.4, 0.8, 1]) # 累积概率
for n in range(N):
wk = np.random.rand()
for k in range(K):
if wk < Pi[k]:
T3[n, k] = 1
break
for k in range(2):
X[n, k] = (np.random.randn() * Sig[T3[n, :] == 1, k]
+ Mu[T3[n, :] == 1, k])
# 用图形显示数据 ------------------------------
def show_data(x):
plt.plot(x[:, 0], x[:, 1], linestyle='none',
marker='o', markersize=6,
markeredgecolor='black', color='gray', alpha=0.8)
plt.grid(True)
# 主处理 ------------------------------------
plt.figure(1, figsize=(4, 4))
show_data(X)
plt.xlim(X_range0)
plt.ylim(X_range1)
plt.show()
np.savez('data_ch9.npz', X=X, X_range0=X_range0,
X_range1=X_range1)
# Mu 和R的初始化 -----------------------------
Mu = np.array([[-2, 1], [-2, 0], [-2, -1]]) # (A)
R = np.c_[np.ones((N, 1), dtype=int), np.zeros((N, 2), dtype=int)] # (B)
# 在图形上显示数据的函数 ---------------------------
def show_prm(x, r, mu, col):
for k in range(K):
# 绘制数据分布
plt.plot(x[r[:, k] == 1, 0], x[r[:, k] == 1, 1],
marker='o',
markerfacecolor=X_col[k], markeredgecolor='k',
markersize=6, alpha=0.5, linestyle='none')
# 以“星形标记”绘制数据的平均值
plt.plot(mu[k, 0], mu[k, 1], marker='*',
markerfacecolor=X_col[k], markersize=15,
markeredgecolor='k', markeredgewidth=1)
plt.xlim(X_range0)
plt.ylim(X_range1)
plt.grid(True)
# ------------------------------
plt.figure(figsize=(4, 4))
R = np.c_[np.ones((N, 1)), np.zeros((N, 2))]
show_prm(X, R, Mu, X_col)
plt.title('initial Mu and R')
plt.show()
运行结果如图 5 右半部分所示。

图5
步骤 1:更新 R
下面更新 R,方法是“使各数据点属于离其最近的中心点所在的簇”

图6
通过这个过程,数据点将被分配到各个类别(图 7 右)。

图7
对所有数据执行这个过程。
# 确定 r (Step 1) -----------
def step1_kmeans(x0, x1, mu):
N = len(x0)
r = np.zeros((N, K))
for n in range(N):
wk = np.zeros(K)
for k in range(K):
wk[k] = (x0[n] - mu[k, 0])**2 + (x1[n] - mu[k, 1])**2
r[n, np.argmin(wk)] = 1
return r
# ------------------------------
plt.figure(figsize=(4, 4))
R = step1_kmeans(X[:, 0], X[:, 1], Mu)
show_prm(X, R, Mu, X_col)
plt.title('Step 1')
plt.show()
步骤 2:更新 μ

图8
下面通过代码清单求 μ,并将结果显示出来。
# 确定 Mu (Step 2) ----------
def step2_kmeans(x0, x1, r):
mu = np.zeros((K, 2))
for k in range(K):
mu[k, 0] = np.sum(r[:, k] * x0) / np.sum(r[:, k])
mu[k, 1] = np.sum(r[:, k] * x1) / np.sum(r[:, k])
return mu
# ------------------------------
plt.figure(figsize=(4, 4))
Mu = step2_kmeans(X[:, 0], X[:, 1], R)
show_prm(X, R, Mu, X_col)
plt.title('Step2')
plt.show()

图9
从图中可以看出,μk 朝着每个分布的中心进行了移动。
至此,算法要进行的计算就介绍完毕了,接下来就是重复步骤 1 和步骤 2 的处理,直到变量的值不再变化。在本例中,经过 6 次重复,变量就不再变化了。
plt.figure(1, figsize=(10, 6.5))
Mu = np.array([[-2, 1], [-2, 0], [-2, -1]])
max_it = 6 # 重复次数
for it in range(0, max_it):
plt.subplot(2, 3, it + 1)
R = step1_kmeans(X[:, 0], X[:, 1], Mu)
show_prm(X, R, Mu, X_col)
plt.title("{0:d}".format(it + 1))
plt.xticks(range(X_range0[0], X_range0[1]), "")
plt.yticks(range(X_range1[0], X_range1[1]), "")
Mu = step2_kmeans(X[:, 0], X[:, 1], R)
plt.show()
我们仔细看一下图 9 中的结果。从图中可以看出,μk 慢慢向 3 个簇的中心移动,每个簇被分配了不同的类别。
一个无监督学习的经典算法——K-means就实现完成了,它具有以下几个优点:
1、原理简单,收敛速度快,这个是业界用它最多的重要原因之一。
2、调参的时候只需要改变k一个参数。
3、算法的可解释度比较强。
如果想系统地学习机器学习,推荐给大家一本入门书籍 《用python动手学机器学习》,除了刚刚介绍的无监督学习,书上还介绍了有监督学习中的回归与分类、神经网络与深度学习的算法与应用、手写数字识别等。本书既有图形、代码,又有详细的数学式推导过程,大大降低了机器学习的学习门槛,即使没有学过Python、数学基础不太好,也可以看懂。
本书是面向机器学习新手的入门书,从学习环境的搭建开始,图文并茂地介绍了学习机器学习所需的Python知识和数学知识,并在此基础上结合数学式、示例程序、插图等,抽丝剥茧般地对有监督学习中的回归与分类、神经网络与深度学习的算法与应用、手写数字识别、无监督学习的算法等进行了介绍。
本书既有图形、代码,又有详细的数学式推导过程,大大降低了机器学习的学习门槛,即使没有学过Python、数学基础不太好,也可以看懂。