简介
在本指南中,我们将看看一个无监督的学习模型,被称为 自组织地图(SOM),以及它在Python中的实现。我们将使用一个RGB颜色的例子来训练SOM,并展示其性能和典型用法。
自组织地图。一般介绍
自组织地图是由Teuvo Kohonen在1982年首次提出的,有时也被称为 科诺恩图.它是人工神经网络的一种特殊类型,它建立了一个训练数据的地图。该地图一般是一个二维的权重矩形网格,但也可以扩展到三维或更高维度的模型。其他网格结构如六边形网格也是可能的。
SOM主要用于数据的可视化,并提供训练实例的快速视觉总结。在一个二维的矩形网格中,每个单元由一个权重向量表示。对于一个经过训练的SOM来说,每个单元的权重代表了几个训练实例的总结。彼此相近的单元格具有相似的权重,类似的例子可以被映射到彼此相邻的小单元中。
下图是对SOM结构的一个粗略说明。
SOM是用竞争性学习来训练的。
竞争性学习是一种无监督学习的形式,在这种学习中,组成元素通过竞争来产生一个令人满意的结果,而且只有一个元素能在竞争中获胜。
当一个训练实例被输入到网格中时,就会确定 ***最佳匹配单元(BMU)***被确定为(竞争获胜者)。BMU是其权重最接近训练实例的单元。
接下来,BMU的权重和邻近BMU的单元的权重被调整以接近输入的训练实例。虽然还有其他有效的训练SOM的变体,但我们在本指南中介绍了最流行和最广泛使用的SOM的实现。
由于我们将使用一些Python例程来演示用于训练SOM的功能,让我们导入一些我们将使用的库。
import numpy as np
import matplotlib.pyplot as plt
训练自组织图背后的算法
下面给出了训练SOM的基本算法。
- 初始化SOM的所有网格权重
- 重复进行,直到达到收敛或最大历时为止
- 洗涤训练实例
- 对于每个训练实例 \(x\)
- 找到最佳匹配单元BMU
- 更新BMU和其相邻单元的权重向量
初始化、寻找BMU和更新权重的三个步骤将在下面的章节中解释。让我们开始吧!
初始化SOM网格
所有SOM网格的权重都可以随机初始化。SOM网格的权重也可以通过从训练数据集中随机选择的例子进行初始化。
你应该选择哪一个?
SOM对地图的初始权重很敏感,所以这个选择会影响整个模型。根据莱斯特大学和西伯利亚联邦大学的Ayodeji和Evgeny进行的一项案例研究。
通过比较在相同条件下RI(随机初始化)最终SOM地图的比例优于PCI(主成分初始化),观察到RI对于非线性数据集的表现相当好。
然而,对于准线性数据集,结果仍然不确定。总的来说,我们可以得出结论,对于本质上的非线性数据集,关于PCI的优势的假设肯定是错误的。
对于非线性数据集,随机初始化优于非随机初始化。对于准线性数据集,还不太清楚什么方法能持续获胜。鉴于这些结果--我们将坚持使用随机初始化。
寻找最佳匹配单元(BMU)
如前所述,最佳匹配单元是SOM网格中最接近训练实例(x\)的单元。找到这个单元的一种方法是计算 \(x\)与网格中每个单元的权重的欧几里得距离。
具有最小距离的单元可以被选为BMU。
需要注意的一点是,欧氏距离不是选择BMU的唯一可能方法。另一种距离测量或相似度量也可以用来确定BMU,选择这个主要取决于你具体建立的数据和模型。
更新BMU和相邻单元的权重向量
A training example (x\)通过将SOM网格的各个单元的权重拉向它而影响这些单元。最大的变化发生在BMU中,当我们远离SOM网格中的BMU时, \(x\)的影响就会减少。对于坐标为 \((i,j)\)的单元,其权重 \(w_{ij}\)在 epoch\(t+1\)时被更新为
其中 \(\Delta w_{ij}^{(t)}\)是要加到 \(w_{ij}^{(t)}\)的变化。它可以被计算为
对于这个表达式。
- \χ(t\)为历时数
- \((g,h)\)是BMU的坐标
- \(\eta\)是学习率
- \(sigma_t\)是半径
- \(f_{ij}(g,h,\sigma_t)\)是邻域距离函数。
在下面的章节中,我们将介绍这个权重训练表达式的细节。
学习率
学习率\(\eta\)是[0,1]范围内的一个常数,决定了权重向量对输入训练实例的步长。对于 \(\eta=0\),权重没有变化,而当 \(\eta=1\)时,权重向量 \(w_{ij}\)取值为 \(x\)。
\(\eta\)在开始时保持较高的权重,并随着历时的进行而逐渐降低。在训练阶段降低学习率的一个策略是使用指数衰减。
其中 \(\lambda<0\) 是衰减率。
为了了解学习率是如何随衰减率变化的,让我们把初始学习率设置为1时的学习率与不同的历时绘制出来。
epochs = np.arange(0, 50)
lr_decay = [0.001, 0.1, 0.5, 0.99]
fig,ax = plt.subplots(nrows=1, ncols=4, figsize=(15,4))
plt_ind = np.arange(4) + 141
for decay, ind in zip(lr_decay, plt_ind):
plt.subplot(ind)
learn_rate = np.exp(-epochs * decay)
plt.plot(epochs, learn_rate, c='cyan')
plt.title('decay rate: ' + str(decay))
plt.xlabel('epochs $t$')
plt.ylabel('$\eta^(t)$')
fig.subplots_adjust(hspace=0.5, wspace=0.3)
plt.show()
邻域距离函数
邻域距离函数由以下公式给出。
其中 \(d((i,j),(g,h))\)是一个单元的坐标 \((i,j)\)与BMU的坐标 \((g,h)\)的距离, \(sigma_t\)是 epoch \(t\)时的半径。通常情况下,欧氏距离被用来计算距离,然而,任何其他距离或相似度量都可以被使用。
由于BMU与自身的距离为零,因此BMU的权重变化减少为。
对于一个与BMU有较大距离的单元\((i,j)\)来说,邻域距离函数减少到一个接近零的值,导致一个非常小的幅度\(\Delta w_{ij}\)。因此,这类单元不受训练实例 \(x\)的影响。因此,一个训练例子只影响BMU和BMU附近的细胞。当我们远离BMU时,权重的变化就会越来越小,直到可以忽略不计。
半径决定了训练实例的影响区域(x\)。一个高的半径值会影响更多的单元,一个较小的半径只影响BMU。一个常见的策略是以大的半径开始,随着历时的进行而减少,即。
这里 \(\beta<0\)是衰变率。与半径相对应的衰变率与与学习率相对应的衰变率对半径的影响相同。为了更深入地了解邻接函数的行为,让我们把它与半径不同值的距离画在一起。在这些图中需要注意的一点是,当距离超过10时,距离函数接近一个接近零的值(\sigma^2 \leq 10\)。
我们将在后面的实施部分利用这一事实来使训练更有效率。
distance = np.arange(0, 30)
sigma_sq = [0.1, 1, 10, 100]
fig,ax = plt.subplots(nrows=1, ncols=4, figsize=(15,4))
plt_ind = np.arange(4) + 141
for s, ind in zip(sigma_sq, plt_ind):
plt.subplot(ind)
f = np.exp(-distance ** 2 / 2 / s)
plt.plot(distance, f, c='cyan')
plt.title('$\sigma^2$ = ' + str(s))
plt.xlabel('Distance')
plt.ylabel('Neighborhood function $f$')
fig.subplots_adjust(hspace=0.5, wspace=0.3)
plt.show()
使用NumPy在Python中实现自组织地图
由于在事实上的标准机器学习库中没有内置的SOM程序。 Scikit-Learn我们将用以下方法来快速实现 NumPy.这个无监督的机器学习模型是非常直接和容易实现的。
我们将把SOM实现为一个二维mxn 网格,因此需要一个三维NumPy 阵列。第三维需要用来存储每个单元的权重。
# Return the (g,h) index of the BMU in the grid
def find_BMU(SOM,x):
distSq = (np.square(SOM - x)).sum(axis=2)
return np.unravel_index(np.argmin(distSq, axis=None), distSq.shape)
# Update the weights of the SOM cells when given a single training example
# and the model parameters along with BMU coordinates as a tuple
def update_weights(SOM, train_ex, learn_rate, radius_sq,
BMU_coord, step=3):
g, h = BMU_coord
#if radius is close to zero then only BMU is changed
if radius_sq < 1e-3:
SOM[g,h,:] += learn_rate * (train_ex - SOM[g,h,:])
return SOM
# Change all cells in a small neighborhood of BMU
for i in range(max(0, g-step), min(SOM.shape[0], g+step)):
for j in range(max(0, h-step), min(SOM.shape[1], h+step)):
dist_sq = np.square(i - g) + np.square(j - h)
dist_func = np.exp(-dist_sq / 2 / radius_sq)
SOM[i,j,:] += learn_rate * dist_func * (train_ex - SOM[i,j,:])
return SOM
# Main routine for training an SOM. It requires an initialized SOM grid
# or a partially trained grid as parameter
def train_SOM(SOM, train_data, learn_rate = .1, radius_sq = 1,
lr_decay = .1, radius_decay = .1, epochs = 10):
learn_rate_0 = learn_rate
radius_0 = radius_sq
for epoch in np.arange(0, epochs):
rand.shuffle(train_data)
for train_ex in train_data:
g, h = find_BMU(SOM, train_ex)
SOM = update_weights(SOM, train_ex,
learn_rate, radius_sq, (g,h))
# Update learning rate and radius
learn_rate = learn_rate_0 * np.exp(-epoch * lr_decay)
radius_sq = radius_0 * np.exp(-epoch * radius_decay)
return SOM
让我们来分析一下用于实现自组织地图的关键函数。
find_BMU() 当给定 网格和训练实例 ,返回最佳匹配单元的网格单元坐标。它计算每个单元权重与 之间的欧几里得距离的平方,并返回 ,即具有最小距离的单元坐标。SOM x x (g,h)
update_weights() 函数需要一个SOM网格,一个训练实例x ,参数learn_rate 和radius_sq ,最佳匹配单元的坐标,以及一个step 参数。理论上,SOM的所有单元都会在下一个训练例子中更新。然而,我们在前面表明,对于远离BMU的单元,其变化是可以忽略不计的。因此,我们可以通过只改变BMU附近的单元来使代码更有效率。step 参数指定了更新权重时要改变的左边、右边、上面和下面的最大单元数。
最后,train_SOM() 函数实现了SOM的主要训练过程。它需要一个初始化或部分训练的SOM 网格和train_data 作为参数。它的优点是能够从以前的训练阶段开始训练SOM。此外,还需要learn_rate 和radius_sq 参数以及它们相应的衰减率lr_decay 和radius_decay 。epochs 参数默认设置为10,但如果需要也可以改变。
在一个实际例子上运行自组织地图
训练SOM的一个常见的例子是随机颜色的例子。我们可以训练一个SOM网格,很容易就能看到各种类似的颜色是如何在相邻的单元格中排列的。
彼此相距较远的单元有不同的颜色。
让我们在一个充满随机RGB颜色的训练数据矩阵上运行train_SOM() 函数。
下面的代码初始化了一个训练数据矩阵和一个带有随机RGB颜色的SOM网格。它还显示了训练数据和随机初始化的SOM网格。注意,训练矩阵是一个3000x3的矩阵,然而,我们将其重塑为50x60x3的矩阵,以便于可视化。
# Dimensions of the SOM grid
m = 10
n = 10
# Number of training examples
n_x = 3000
rand = np.random.RandomState(0)
# Initialize the training data
train_data = rand.randint(0, 255, (n_x, 3))
# Initialize the SOM randomly
SOM = rand.randint(0, 255, (m, n, 3)).astype(float)
# Display both the training matrix and the SOM grid
fig, ax = plt.subplots(
nrows=1, ncols=2, figsize=(12, 3.5),
subplot_kw=dict(xticks=[], yticks=[]))
ax[0].imshow(train_data.reshape(50, 60, 3))
ax[0].title.set_text('Training Data')
ax[1].imshow(SOM.astype(int))
ax[1].title.set_text('Randomly Initialized SOM Grid')
现在让我们来训练SOM,并每隔5个 epochs检查一次,作为对其进展的快速概述。
fig, ax = plt.subplots(
nrows=1, ncols=4, figsize=(15, 3.5),
subplot_kw=dict(xticks=[], yticks=[]))
total_epochs = 0
for epochs, i in zip([1, 4, 5, 10], range(0,4)):
total_epochs += epochs
SOM = train_SOM(SOM, train_data, epochs=epochs)
ax[i].imshow(SOM.astype(int))
ax[i].title.set_text('Epochs = ' + str(total_epochs))
上面的例子非常有趣,因为它显示了网格是如何自动排列RGB颜色的,从而使同一颜色的各种色调在SOM网格中紧紧靠在一起。这种安排早在第一个纪元就发生了,但这并不理想。我们可以看到,SOM在大约10个历时中收敛,在随后的历时中变化较少。
学习率和半径的影响
为了看看不同的学习率和半径的学习率是如何变化的,我们可以从相同的初始网格开始,运行SOM 10个epochs。下面的代码针对三个不同的学习率值和三个不同的半径对SOM进行训练。
每次模拟5个历时后,SOM就会呈现出来。
fig, ax = plt.subplots(
nrows=3, ncols=3, figsize=(15, 15),
subplot_kw=dict(xticks=[], yticks=[]))
# Initialize the SOM randomly to the same state
for learn_rate, i in zip([0.001, 0.5, 0.99], [0, 1, 2]):
for radius_sq, j in zip([0.01, 1, 10], [0, 1, 2]):
rand = np.random.RandomState(0)
SOM = rand.randint(0, 255, (m, n, 3)).astype(float)
SOM = train_SOM(SOM, train_data, epochs = 5,
learn_rate = learn_rate,
radius_sq = radius_sq)
ax[i][j].imshow(SOM.astype(int))
ax[i][j].title.set_text('$\eta$ = ' + str(learn_rate) +
', $\sigma^2$ = ' + str(radius_sq))
上面的例子显示,对于半径值接近零的情况(第一列),SOM只改变单个单元,而不改变相邻的单元。因此,无论学习率如何,都无法创建一个合适的地图。对于较小的学习率也会遇到类似的情况(第一行,第二列)。与其他机器学习算法一样,理想的训练需要一个良好的参数平衡。
结论
在本指南中,我们讨论了SOM的理论模型和它的详细实现。我们在RGB颜色上演示了SOM,并展示了同一颜色的不同色调如何在二维网格上组织起来。
虽然SOM在机器学习界已不再流行,但它们仍然是数据总结和可视化的好模型。