65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 中的 Kohonen 自组织映射及其在计算机视觉领域的应用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (33投票s)

2007年11月21日

GPL3

18分钟阅读

viewsIcon

188987

downloadIcon

7118

本文演示了自组织映射聚类方法,用于无监督 AI 分类任务,并在计算机视觉领域有面部聚类和识别的应用示例。

Clusters of similar faces

目录

引言

人工智能方法分为监督式和非监督式两类。前者用于训练分类器以区分数据,当你知道训练集中存在的类别时使用;后者在你面对未知数据并想发现其结构时使用。也就是说,你想获得一个二维或三维的低维映射,从中可以轻松找出初始高维数据如何组合在一起。例如,在经典的鸢尾花(IRIS)数据集中,有 4 维对象的 3 个类别,假设你事先不知道它们有 3 个。如果数据的维度是 2 或 3,你可以将它们绘制成散点图并识别出有多少个类别。但如果维度是 1000 呢?你可以认为自组织映射(SOM)也是一种降维技术。它将未知数据集中的相似样本组合在一起,也就是说,它会调整其权重系数以更接近你输入的数据样本。因此,它有效地学习了数据中存在的聚类。对于鸢尾花数据集,一些 SOM 节点会类似于setosa,一些类似于virgi,另一些类似于versi,相似的条目将在 SOM 的网格上分组在一起。如果你比较相邻 SOM 节点权值之间的距离,相似的节点之间的距离会更近,而不同类别的节点之间的距离会更大。绘制出距离后,你将获得聚类的边界,然后只需将未知数据样本分类到发现的类别中。

在这个 C++ 库中,我引入了数据归一化。如果某些特征的数值范围明显增加,你需要对其进行归一化(例如,人的身高和体重。第一个特征范围约为 1.50 到 2.20 米,第二个特征范围是 50 - 150 千克。后者占主导地位,你的聚类结果将偏向体重)。除了欧氏距离,我还使用了平方和距离(SOSD)、出租车距离和向量之间的角度。我尚未完成马氏距离的实现,但你可以轻松添加代码。只需包含你使用的训练数据的协方差矩阵的逆矩阵即可。此代码支持的数据文件格式在我的 C++ 反向传播人工神经网络 文章中有描述。

该库的另一个特点是你可以创建任意维度的 SOM。通常是二维或三维地图,但用我的代码你可以为你的地图选择任意数量的维度。

背景

你可以在我学习该主题时使用的网站上了解 SOM 技术和应用:Kohonen 的自组织特征映射自组织网络,以及自组织映射 AI 图片。在继续阅读我的文章之前,请先阅读它们。

Using the Code

库的实现是以控制台应用程序的形式提供的,以便于使用。控制台的帮助行如下:

 1 t-train
 2 net.som
 3 class.txt
 4 epochs
  5 [test set]
 6 [slow] [slow=0]fast=1 training
 7 [dist] distance [0-euclidian],1-sosd,2-taxicab,3-angle
 8 [norm] normalize input data [0-none],1-minmax,2-zscore,3-softmax,4-energy

 1 r-run
 2 net.som
 3 class.txt

我在演示下载中附带了 train.bat 文件,因此你可以立即开始聚类。作为简单的例子,我使用了红、绿、蓝三种颜色作为 rgbs.txtrgbs1.txt 文件中的三维向量。训练 SOM 网络的可执行命令行如下:

>som.exe t rgb.som rgbs.txt 500

使用 500 个轮次训练 rgbs.txt 数据上的 rgb.som 网络。6 到 8 个参数是可选的,你可以省略它们。第 5 个参数仅在你将网络用于分类 2 个不同类别时使用。你可以在其中提供测试数据,以便在训练后立即获得性能准确度,或者提供一个空文件。后一种情况将使用训练集中随机的一半进行测试。否则,省略第 5 个参数,并在 4 之后按相同顺序使用 6 - 8。

>som.exe t rgb.som rgbs.txt 500  1 2 2

这与前面的例子相同,但使用了快速训练模式(更多描述见下文)、出租车距离度量和 zscore 数据归一化。

训练后,将保存 1 或 2 个文件,具体取决于你是对未知类别还是已知类别的数据进行聚类。相邻 SOM 节点之间的距离图将保存到 rgb_map 文件。如果你事先不知道样本的类别,请在向量名称后使用以下条目,不带类别标记:

name1
x1 x2 x3 ... xN
name2
x1 x2 x3 ... xN
...

使用距离图文件来发现聚类的边界。它将包含与 SOM 网格相同大小的二维向量。最高值对应于分隔不同聚类的脊。在这种情况下,将只保存 rgb_2.som 文件,其中二维网格的 SOM 节点类别标记为零。你已经从距离图文件中定义了你的聚类。现在,在 rgb_2.som 文件中用所需的类别 1、2、3 ... N 替换 SOM 网格中的零。要获得更详细的解释,我将在下一个案例中详细阐述,即如果你知道你的数据的类别,并想生成一个多类分类器。

rgbs.txt 文件包含红、绿、蓝三种颜色(3D 向量)的 3 个类别。训练后,将创建 2 个 SOM 文件:rgb_1.somrgb_2.som。第一个使用投票方案(更多描述见下文)来确定 SOM 节点基于训练数据的类别成员资格,第二个使用直接方案为每个 SOM 节点确定其基于训练集的类别。如果你在文本编辑器中打开它们,你会发现一个 50x50 的二维图,其中数字 1、2 和 3 分组在一起。这是你数据的聚类。相似的向量被分组在一起。现在使用 Matlab 绘制 rgb_map,使用 imshow(rgb_map, []) colormap('jet')

distance map

你看到 3 个分离的蓝色区域,它们是你数据中发现的 3 个聚类。你二维图(50x50 条目)中的 3 个类别(rgb_1.somrgb_2.som)与这些区域完美匹配。现在,如果你只有 rgb_2.som 文件,其中类别未知(50x50 二维图中的 0),你将按照我上面描述的方式,用你在 rgb_map 图中找到的 3 个不同类别替换零,从而得到你的分类器。

SOM 的节点包含三维权重向量,这些向量被调整以模仿训练数据。利用 RGB 颜色数据的优势,我们可以直接排列 50x50x3 的 RGB 颜色数据并进行绘制。

SOM node's weights

现在你可以看到每个节点的权重与 rgbs.txt 文件中的数据相似。

在快速模式训练中,我不会改变超出最佳匹配单元(BMU)节点半径范围的节点权重,而在前面的例子中,我使用指数衰减的学习规则来修改所有节点的权重。然而,快速模式训练提供了更清晰的边界。

distance map

下面的 RGB 颜色数据是 50x50x3 的。

SOM node's weights

现在你可以选择随机的 RGB 颜色条目并尝试对它们进行聚类。在下一张图中,显示了通过聚类 2000 个随机 RGB 向量得到的 50x50x3 SOM 地图。

SOM node's weights

相似的颜色被聚类在一起。

接下来,我将提供项目类别的更详细描述。

C++ SOM 库包含 2 个类。

  • 节点
  • SOM

Node 是构成 SOM 地图的单元。它在构造时被初始化,包含特定的 weights 向量(长度为 weights_number),其在 N 维 SOM 地图中的坐标 coords 向量(包含 coords_number 个坐标,如 x, y, z...)以及可选的类别成员资格 class_

  • Node::Node(const float *weights, int weights_number, 
        const float *coords, int coords_number, int class_ = 0);

对于二维 SOM 地图,每个节点有两个坐标(x, y);对于三维地图(x, y, z),依此类推。但除非你仅将其用作分类器,否则你将无法可视化四维及以上的 SOM。weights_number 等于你用于聚类或分类的数据的维度。对于 RGB 颜色三元组,数据向量是三维的。

为了确定节点权重向量与数据集中特定数据向量的距离,定义了 get_distance() 函数。

  • inline float Node::get_distance(const float *vec, 
        enum DistanceMetric distance_metric, const float **cov = 0) const;

distance_metric 是用于计算距离的指标之一:enum DistanceMetric {EUCL, SOSD, TXCB, ANGL, MHLN}; 分别代表欧氏距离、平方和距离、出租车距离、角度距离或马氏距离。后者保留给未来的代码开发(在训练前,你需要估计训练集的协方差矩阵的逆矩阵,并将其作为可选参数 cov 传递)。

inline float Node::get_distance
    (const float *vec, enum DistanceMetric distance_metric, const float **cov) const
{
        float distance = 0.0f;
        float n1 = 0.0f, n2 = 0.0f;

        switch (distance_metric) {
        default:
        case EUCL:   //euclidian
                if (m_weights_number >= 4) {
                        distance = mse(vec, m_weights, m_weights_number);
                } else {
                        for (int w = 0; w < m_weights_number; w++)
                                distance += (vec[w] - m_weights[w]) *
                            (vec[w] - m_weights[w]);
                }
                return sqrt(distance);

        case SOSD:   //sum of squared distances
                if (m_weights_number >= 4) {
                        distance = mse(vec, m_weights, m_weights_number);
                } else {
                        for (int w = 0; w < m_weights_number; w++)
                                distance += (vec[w] - m_weights[w]) *
                (vec[w] - m_weights[w]);
                }
                return distance;

        case TXCB:   //taxicab
                for (int w = 0; w < m_weights_number; w++)
                        distance += fabs(vec[w] - m_weights[w]);
                return distance;

        case ANGL:   //angle between vectors
                for (int w = 0; w < m_weights_number; w++) {
                        distance += vec[w] * m_weights[w];
                        n1 += vec[w] * vec[w];
                        n2 += m_weights[w] * m_weights[w];
                }
                return acos(distance / (sqrt(n1)*sqrt(n2)));

        //case MHLN:   //mahalanobis
                //distance = sqrt(m_weights * cov * vec)
                //return distance
        }
}

mse() 只是 SSE 优化函数,用于返回两个向量之间的均方误差。

为了使节点权重向量适应你的训练集中特定的数据向量,使用了 train() 函数。

  • inline void Node::train(const float *vec, float learning_rule);
inline void Node::train(const float *vec, float learning_rule)
{
        for (int w = 0; w < m_weights_number; w++)
                m_weights[w] += learning_rule * (vec[w] - m_weights[w]);
}

learning_rule 允许你控制权重的改变量。根据我在背景部分建议的文章,它从大约 0.9 开始,并在训练过程中呈指数递减。

有两个构造函数可用于从文件创建或加载 SOM 地图:

  • SOM::SOM(int dimensionality, int *dimensions, int weights_per_node, 
        enum Node::DistanceMetric distance_metric, float *add = 0, 
            float *mul = 0);
  • SOM::SOM(const wchar_t *fname);

第一个构造函数可以让你创建一个 N 维 SOM 地图,其中网格的维度数为 dimensionality,每个维度的尺寸由 dimensions 指定。weights_per_node 是你的训练数据样本的维度,distance_metric 名称不言自明,而 addmul 是可选的归一化向量。输入到 SOM 的每个数据向量都将按 x(i) = (x(i) + add(i)) * mul(i) 进行归一化。默认情况下,add 填充零,mul 填充一,因此不进行归一化。节点权重将使用随机数初始化。

int dimensions[] = {50, 50};
//create 2 dimensional 50 by 50 SOM map for 3 dimensional data clustering
//(RGB or any other)
SOM rgb_som(2, dimensions, 3, Node::EUCL);

int dimensions[] = {10, 10, 10, 10}
//create 4 dimensional 10x10x10x10 SOM map for 10 dimensional data clustering
SOM rgb_som(4, dimensions, 10, Node::ANGL);

你也可以使用第二个构造函数从文件加载 SOM 网络。你可以使用简化文件格式,仅指定 SOM 维度、维度大小和每个节点的权重,如演示 zip 文件中的 rgb.som 文件所示;或者使用包含实际权重系数的完整文件格式。简化文件格式允许在聚类前生成随机权重值。

>rgb.som file, 2D SOM 50 by 50 with 3 weights per node
2
50 50
3

完整 SOM 文件格式以 5x5 的二维 SOM 地图为例,用于聚类 rgbs.txt 数据。

2
5 5
3
Eucl

 2 2 2 3 3
 2 2 2 3 3
 1 1 1 3 3
 1 1 1 3 3
 1 1 1 1 3

None
0        1
0        1
0        1

0 0
0.00398814
0.987988
0.0100357

1 0
0.01
0.94
0.03

2 0
0.00902465
0.360081
0.618271

3 0
0.01
0.02
0.97

4 0
0.0133332
0.00336507
0.996667

0 1
0.0198603
0.956393
0.0118624

1 1
3.37824e-013
0.91
0.01

2 1
0.0197248
0.368034
0.581373

3 1
0.0191355
0.0162165
0.950495

4 1
0.0150343
0.00503427
0.975034

0 2
0.753054
0.228725
0.0101103

1 2
0.643217
0.28021
0.046389

2 2
0.222105
0.0906172
0.651532

3 2
0.03
0.01
0.92

4 2
0.03
0.01
0.94

0 3
0.975034
0.0150343
0.00503427

1 3
0.94
0.03
0.01

2 3
0.742754
0.0188995
0.198852

3 3
0.152863
0.0109908
0.787188

4 3
2.79205e-013
0.01
0.91

0 4
0.996667
0.0133332
0.00336507

1 4
0.97
0.01
0.02

2 4
0.915034
0.0151028
0.01

3 4
0.65243
0.014252
0.276088

4 4
0.0573598
0.010243
0.871152

Eucl 表示用于计算 BMU 节点的欧氏距离。其他可能的距离字符串在 SOM::g_distance[5][5] 中定义。接下来是 5x5 节点类别成员资格标记。在这种情况下,1 是红色向量,2 是绿色,3 是蓝色。如果你事先不知道未知数据的类别,则为 0,表示未知类别节点。None 是输入 SOM 数据的归一化方式,第一列是 add 向量,第二列是 mul 向量,关于归一化公式请参阅上文。归一化字符串在 SOM::g_normalization[5][5] 中定义,包括无归一化、minmax、zscore、sigmoid 和 energy。接下来是节点在 SOM 网格中的坐标及其权重向量。因此,(0,0) 是 SOM 二维网格中 x = 0 和 y = 0 位置的节点,依此类推。

构造函数调用后,int SOM::status(void) 用于确定你是否成功加载文件,负值表示错误,零表示成功,正值表示随机权重。

你可以通过这些函数外部指定距离度量、训练模式和归一化参数:

  • inline void SOM::set_distance_metric(enum Node::DistanceMetric distance_metric);
  • inline void SOM::set_train_mode(enum TrainMode train_mode);
  • void SOM::compute_normalization(PREC rec, enum Normalization norm);

PREC 是一个数据结构,你可以在其中使用控制台主体中定义的 read_class() 函数加载训练数据,有关详细信息请参阅 stdafx.h

创建 SOM 地图后,你就可以开始聚类数据了。

  • void SOM::train(const vector<float *> *vectors, float R, float learning_rule);

train() 函数运行一个训练周期的训练。它使用存储在 vectors std 数组中的数据向量,并在 BMU 节点的 R 半径范围内使用 learning_rule 调整其权重。我在控制台代码中以这种方式使用 Rlearning_rule 的指数衰减:

float R, R0 = som->R0();
float nrule, nrule0 = 0.9f;

int epochs = _wtoi(argv[4]);
float N = (float)epochs;
int x = 0;  //0...N-1

while (epochs) {
        //exponential shrinking
        R = R0 * exp(-10.0f * (x * x) / (N * N));          //radius shrinks over time
        //learning rule shrinks over time
        nrule = nrule0 * exp(-10.0f * (x * x) / (N * N));

        x++;

        som->train(&train_vectors, R, nrule);

        wprintf(L"  epoch: %d    R: %.2f nrule: %g \n", (epochs-- - 1), R, nrule);

        if (kbhit() && _getwch() == 'q') //quit program ?
                epochs = 0;
}

SOM::R0() 返回 SOM 地图最长维度大小的一半。对于 20x50 的 SOM 地图,初始半径等于 50 / 2 = 25,在此范围内将更新 BMU 节点附近的邻居节点。

float SOM::R0() const
{
        float R = 0.0f;

        for (int i = 0; i < m_dimensionality; i++)
                if ((float)m_dimensions[i] > R)
                        R = (float)m_dimensions[i];

        return R / 2.0f;
}

训练函数如下所示:

void SOM::train(const vector<float *> *vectors, float R, float learning_rule)
{
        for (int n = 0; n < (int)vectors->size(); n++) {

                const float *pdata = normalize(vectors->at(n));

                //get best node
                Node *bmu_node = get_bmu_node(pdata);
                const float *p1 = bmu_node->get_coords();

                if (R <= 1.0f)  //adjust BMU node only
                        bmu_node->train(pdata, learning_rule);
                else {
                        //adjust weights within R
                        for (int i = 0; i < get_nodes_number(); i++) {

                                const float *p2 = m_nodes[i]->get_coords();
                                float dist = 0.0f;

                                //dist = sqrt((x1-y1)^2 + (x2-y2)^2 + ...)
                                //distance to node in SOM grid
                                for (int p = 0; p < m_dimensionality; p++)
                                        dist += (p1[p] - p2[p]) * (p1[p] - p2[p]);
                                dist = sqrt(dist);

                                if (m_train_mode == FAST && dist > R)
                                        continue;

                                float y = exp(-(1.0f * dist * dist) / (R * R));
                                m_nodes[i]->train(pdata, learning_rule * y);
                        }
                }
        }
}

现在你可以看看 SLOWFAST 训练模式。如果在 FAST 训练模式下,所选节点的到 BMU 节点的距离超过当前 R,则所选节点不会向输入训练向量调整其权重。当 R 低于 1.0f 时,将执行精调阶段,此时只有 BMU 节点的权重会得到调整。

normalize() 函数仅使用指定的归一化类型对输入数据向量进行归一化。

const float* SOM::normalize(const float *vec)
{
        switch (m_normalization) {
        default:
        case NONE:
                for (int x = 0; x < get_weights_per_node(); x++)
                        m_data[x] = vec[x];
                break;

        case MNMX:
                for (int x = 0; x < get_weights_per_node(); x++)
                        m_data[x] = (0.9f - 0.1f) *
                        (vec[x] + m_add[x]) * m_mul[x] + 0.1f;
                break;

        case ZSCR:
                for (int x = 0; x < get_weights_per_node(); x++)
                        m_data[x] = (vec[x] + m_add[x]) * m_mul[x];
                break;

        case SIGM:
                for (int x = 0; x < get_weights_per_node(); x++)
                        m_data[x] = 1.0f / (1.0f +
                        exp(-((vec[x] + m_add[x]) * m_mul[x])));
                break;

        case ENRG:
                float energy = 0.0f;
                for (int x = 0; x < get_weights_per_node(); x++)
                        energy += vec[x] * vec[x];
                energy = sqrt(energy);

                for (int x = 0; x < get_weights_per_node(); x++)
                        m_data[x] = vec[x] / energy;
                break;
        }
        return m_data;
}

你使用 compute_normalization() 函数预先计算的归一化参数,可以选择以下可用归一化方法之一:enum Normalization {NONE, MNMX, ZSCR, SIGM, ENRG};

如果你只是聚类未知数据向量,那么你可以继续保存 SOM 网络和距离图。

  • int save(const wchar_t *fname) const;
  • int save_2D_distance_map(const wchar_t *fname) const;

目前,距离图仅支持二维 SOM 地图,因为更高维度的地图你将无法可视化。为了创建距离图,会计算相邻节点权重之间的均方误差。如果节点属于同一聚类,它们的权重不会相差太大。相反,对于位于聚类边界上的相邻节点,它们的权重均方误差值会更高。

int SOM::save_2D_distance_map(const wchar_t *fname) const
{
        int D = 2;
        float min_dist = 1.5f;

        if (get_dimensionality() != D)
                return -1;

        FILE *fp = _wfopen(fname, L"wt");
        if (fp != 0) {
                int n = 0;
                for (int i = 0; i < m_dimensions[0]; i++) {
                        for (int j = 0; j < m_dimensions[1]; j++) {
                                float dist = 0.0f;
                                int nodes_number = 0;
                                const Node *pnode = get_node(n++);
                                for (int m = 0; m < get_nodes_number(); m++) {
                                        const Node *node = get_node(m);
                                        if (node == pnode)
                                                continue;
                                        float tmp = 0.0;
                                        for (int x = 0; x < D; x++)
                                                tmp += pow(*(pnode->get_coords() + x) -
                                                       *(node->get_coords() + x), 2.0f);
                                        tmp = sqrt(tmp);
                                        if (tmp <= min_dist) {
                                                nodes_number++;
                                                dist += pnode->get_distance
                                                (node->m_weights, m_distance_metric);
                                        }
                                }
                                dist /= (float)nodes_number;
                                fwprintf(fp, L" %f", dist);
                        }
                        fwprintf(fp, L"\n");
                }
                fclose(fp);
                return 0;
        }
        else
                return -2;
}

现在,如果你想对具有已知类别成员资格的数据进行聚类,并找出数据是否根据成员资格分组,则需要在聚类过程完成后,将类别分配给 SOM 的节点。你可以使用投票方案或直接类别分配方案。

  • void vote_nodes_from(PREC rec);
  • void assign_nodes_to(PREC rec);

使用投票方案,你将训练集中的向量输入到 SOM。对于每个向量,只有一个 BMU 节点与之最接近。你增加该向量类别在 BMU 节点上的投票计数。所有训练向量都被输入到 SOM 后,根据特定节点上某个类别的多数投票,将类别成员资格分配给该节点。假设你的数据集中存在 3 个类别,某个节点对第 1 类“激发”了 3 次,对第 2 类激发了 20 次,对第 3 类激发了 5 次。根据多数投票,你将其分配给第 2 类。你在分类时信任该节点的程度为 20 / (3 + 20 + 5) = 0.71(满分为 1.0)。

void SOM::vote_nodes_from(PREC rec)
{
        //rec->clsnum = [cls 1][cls 2] ... [cls N]   N entries
        //example: 0,1,2  3,1,2   1,4,10 missed classes

        //clear votes for classes of all nodes
        for (int n = 0; n < get_nodes_number(); n++)
                m_nodes[n]->clear_votes((int)rec->clsnum.size());

        while (true) { //until unclassified nodes m_class=0
                //add vote to a best node for a given class
                for (int y = 0; y < (int)rec->entries.size(); y++) {
                        if (rec->entries[y] == 0)
                                continue;

                        const float *pdata = normalize(rec->entries[y]->vec);
                        Node *pbmu_0node = get_bmu_0node(pdata);

                        //no more m_class=0 nodes
                        if (pbmu_0node == 0)
                                return;

                        //check class location in REC->clsnum[] array
                        int c = rec->entries[y]->cls;
                        for (int i = 0; i < (int)rec->clsnum.size(); i++) {
                                if (rec->clsnum[i] == c) {
                                        c = i;
                                        break;
                                }
                        }

                        pbmu_0node->add_vote(c);
                }

                //assign class from the max number of votes for a class
                for (int n = 0; n < get_nodes_number(); n++) {
                        if (m_nodes[n]->get_class() == 0)
                                m_nodes[n]->evaluate_class(&rec->clsnum[0],
                                (int)rec->clsnum.size());
                }
        }
}

使用直接方案,你只需在一个单次遍历中浏览 SOM 地图的每个节点,并确定它与训练集中的哪个向量最接近。将该向量的类别分配给该节点。

void SOM::assign_nodes_to(PREC rec)
{
        //run thru nodes and get best vector matching
        for (int n = 0; n < get_nodes_number(); n++) {
                m_nodes[n]->clear_votes();

                int index = 0;
                float mindist = FLT_MAX, dist;
                for (int i = 0; i < (int)rec->entries.size(); i++) {
                        if (rec->entries[i] == 0)
                                continue;
                        const float *pdata = normalize(rec->entries[i]->vec);
                        if ((dist = m_nodes[n]->get_distance
                               (pdata, m_distance_metric)) < mindist) {
                                mindist = dist;
                                index = i;
                        }
                }

                m_nodes[n]->set_class(rec->entries[index]->cls);
        }
}

现在,最后一个函数是如何使用你训练过的、已分配类别的 SOM 来对新数据进行分类。

  • const Node *SOM::classify(const float *vec);

该函数返回一个“激发”你输入的向量 vec 的 BMU 节点。你可以找出该节点的权重、类别成员资格、SOM 网格中的节点坐标以及如果你使用了投票方案,你信任该节点的程度。

  • inline int Node::get_class(void) const;
  • inline const float* Node::get_coords() const;
  • inline const float* Node::get_weights() const;
  • inline float Node::get_precision() const;

用于自然图像和人脸聚类的 SOM 应用

现在我想向你展示如何利用它在真实场景的图像数据中发现未知数据集的模式(但你可以使用你收集的任何其他类型的数据:音频、生理(ECG、EEG、EMG)、地质、其他任何维度的数据)。

假设你正在使用人脸检测应用程序,无论是来自我的 带肤色和运动分析的人脸检测 C++ 库 文章,还是其他方法。你在一些人行道、商店、机场、公交车站等地方运行它,它为你生成了白天或更长时间内看到的人的 1000、10000 或更多人脸样本。你将无法手动浏览它们来识别它看到了多少不同的人!但如果你知道至少有 100 个不同的人可能出现在那个地方,你可以安排一个由至少 100 个节点组成的 SOM 地图,并在几秒钟内将你的 10000 个样本聚类到二维地图上,并立即看到所有这些人。

在我的人脸检测文章中,我使用了大约 1800 张我收集的来自几个人的脸部样本。现在,我可以组成一个 1x20 的 SOM 网络,包含 20 个节点和 361 维的权重。我使用了 19x19 的人脸矩形,SOM 节点是 1 维向量。如果你将 19x19 矩形的列串联起来,你将得到一个 361 维的 1D 向量。现在,如果你想绘制 SOM 的节点 361 维权重向量,只需从中取每 19 个样本,并将它们以列顺序放回 19x19 矩形中。你可以以这种方式对你想聚类的任何维度的数据进行操作。

我首次使用 1x20 的 SOM 地图,所以找到的人脸将被聚类成一条线。现在,按照我上面描述的方式绘制节点的权重向量。在训练过程中,正如你可能已经了解到的,权重会被调整以在欧氏距离或其他距离方面接近你的数据集中的样本。

Found faces from my data set

为了节省文章空间,我将它们绘制在几行中,但如果你将每一行连接起来,你将得到一个 1x25 的节点行。现在从左上角开始,向右下角浏览。你将看到 SOM 在训练过程中聚类的不同人脸模式。

现在回到更熟悉的 5x5 节点的二维 SOM 地图。如果你看一下下面显示的图片,你会看到相似的人脸是如何在几个节点的聚类中放在一起的。

Found faces from my data set

如果我们组成一个包含更多节点的 SOM 地图,例如 10x10,我们将能够发现数据集中存在更多不同的人脸,并且还可以绘制距离图。但相似的人脸仍然会被放在一起聚类,所以你可以轻松发现不同的人。在下面的图片中,显示了包含 100 个节点的 SOM,用于聚类所使用的 1800 张人脸样本不足。

Found faces from my data set

下面显示了 10x10 SOM 地图的聚类人脸的分隔边界的距离图。

Distance map

虽然它相当模糊,但你可以使用大约 50x50 的 SOM 地图来获得更清晰的边界,就像在 RGB 颜色示例中一样。为了节省你的工作,我让我的计算机在 50x50 节点的二维 SOM 地图上聚类人脸。聚类 1800 张人脸样本大约花费了 15 分钟。

Distance map

你可以从上面的图片得出结论,数据集中存在 3 个完全不同的人。现在,如果你将属于该区域的 0 节点标记为第 1、2、3 类,并运行该 SOM 网络来分类你的 1800 张人脸样本,你就会找出那些人的样本。然而,这 3 个聚类也包含几个次聚类,其边界用亮蓝色表示。但由于距离不是很大,与热红色边界相比,人脸的差异并不剧烈。用对数尺度绘制的相同的距离图能更清晰地显示其余的聚类。

Distance map (log scale)

不可能一次性绘制 50x50 的权重 19x19 人脸向量,这需要一些专门的、具有放大能力的绘图。

现在我们可以尝试将我训练集中的人脸和非人脸样本一起聚类,并确定分类准确度,因为我们事先知道哪个样本属于人脸或非人脸类别。

下一张图片显示了人脸和非人脸样本的 SOM 聚类结果。

Face, non-face clusters

由于 SOM 地图相对较小,只有 36 个节点,并且聚类了大约 10000 个样本,因此分类结果相当差。clusters_1.som 显示了投票方案的结果,clusters_2.som 显示了直接方案的结果,用于为它聚类的训练集的节点分配类别成员资格。

SOM clusters_1.som precision: 0.944137

classification results: clusters_1.som

 train set: 894 8927
   sensitivity: 44.63
   specificity: 98.67
   +predictive: 77.03
   -predictive: 94.68
      accuracy: 93.75

classification results: clusters_2.som

 train set: 894 8927
   sensitivity: 60.29
   specificity: 95.78
   +predictive: 58.84
   -predictive: 96.01
      accuracy: 92.55

你可能从自然图像中获得的聚类结果(如上图所示)中观察到的更重要的概念是,节点权重向量类似于我在人脸检测文章中获得的 PCA 投影基。它们可以直接用作投影基来降低维度,但需要考虑如何将它们与要投影的数据正确卷积。这样,你就可以使用 SOM 作为一种工具来发现像 PCA、LDA、ICA 等方法那样的线性投影基。

自适应图像分割

SOM 可以应用于学习图像的颜色聚类。选取一张随机图片(例如,一只鸟)并对颜色进行聚类,相似的颜色将被分组在一起,并使用距离图,我们可以识别边界并将图像分割成特定的颜色区域。

Bird picture

我组建了一个 50x50 节点的 SOM,并对上图中的颜色进行了聚类。

50x50 SOM of the Bird picture

现在下面显示了距离图。

Distance map

可以看出,鸟喙的橙色调被分组在一起,并被距离图上的热红色边界很好地分开了。白色色调也聚集在一个点上。其余的颜色也按相似性分组,但分隔边界不像距离图上的亮蓝色所示那样高。

© . All rights reserved.