如何使 k-means 算法发挥作用
How to make k-means algorithm functional
我在 javascript 中有一个非常基本的 k-means 实现(我知道,但它需要在浏览器中 运行)。我想了解的是 - 如何使它更具功能性?
它目前充满了循环,并且极难理解/推理,代码如下:
export default class KMeans {
constructor(vectors, k) {
this.vectors = vectors;
this.numOfVectors = vectors.length;
this.k = k || bestGuessK(this.numOfVectors);
this.centroids = randomCentroids(this.vectors, this.k);
}
classify(vector, distance) {
let min = Infinity;
let index = 0;
for (let i = 0; i < this.centroids.length; i++) {
const dist = distance(vector, this.centroids[i]);
if (dist < min) {
min = dist;
index = i;
}
}
return index;
}
cluster() {
const assigment = new Array(this.numOfVectors);
const clusters = new Array(this.k);
let movement = true;
while (movement) {
// update vector to centroid assignments
for (let i = 0; i < this.numOfVectors; i++) {
assigment[i] = this.classify(this.vectors[i], euclidean);
}
// update location of each centroid
movement = false;
for (let j = 0; j < this.k; j++) {
const assigned = [];
for (let i = 0; i < assigment.length; i++) {
if (assigment[i] === j) assigned.push(this.vectors[i]);
}
if (!assigned.length) continue;
const centroid = this.centroids[j];
const newCentroid = new Array(centroid.length);
for (let g = 0; g < centroid.length; g++) {
let sum = 0;
for (let i = 0; i < assigned.length; i++) {
sum += assigned[i][g];
}
newCentroid[g] = sum / assigned.length;
if (newCentroid[g] !== centroid[g]) {
movement = true;
}
}
this.centroids[j] = newCentroid;
clusters[j] = assigned;
}
}
return clusters;
}
}
当然可以。
你可以从这个开始:
classify(vector, distance) {
let min = Infinity;
let index = 0;
for (let i = 0; i < this.centroids.length; i++) {
const dist = distance(vector, this.centroids[i]);
if (dist < min) {
min = dist;
index = i;
}
}
return index;
}
为什么这是一个成员函数?纯函数 const classify = (centroids, vector, distance) => {...}
不是更干净吗?
然后为了实现,让我们稍微更改一下 distance
签名。如果我们将它柯里化为 const distance = (vector) => (centroid) => {...}
,我们就可以写
const classify = (centroids, vector, distance) =>
minIndex (centroids .map (distance (vector)))
如果 distance
API 不在我们的控制范围内,那就更难了:
const classify = (centroids, vector, distance) =>
minIndex (centroids .map (centroid => distance (vector, centroid)))
当然,我们还没有写 minIndex
,但我们已经分解了问题以使用更有意义的抽象。 minIndex
并不难写。你可以像原来的 classify
函数那样强制执行,或者像这样:
const minIndex = (xs) => xs.indexOf (Math.min (...xs))
请注意,distance
在这里是一个有点误导的名称。我不得不更仔细地阅读它,因为我假设这样的名字代表……,好吧,一段距离。相反,它是一个用于计算距离的函数。也许名称 metric
或类似 distanceFunction
、distanceFn
或 distanceImpl
的名称会更明显。
现在让我们继续这一点:
const newCentroid = new Array(centroid.length);
for (let g = 0; g < centroid.length; g++) {
let sum = 0;
for (let i = 0; i < assigned.length; i++) {
sum += assigned[i][g];
}
newCentroid[g] = sum / assigned.length;
if (newCentroid[g] !== centroid[g]) {
movement = true;
}
}
此代码有两个职责:创建 newCentroid
数组,并在任何值更改时更新 movement
的值。
让我们把这两个分开。
首先,创建新的质心。我们可以将嵌套的 for
-loop 清理成这样:
const makeNewCentroid = (centroid, assigned) =>
centroid .map ((c, g) => mean (assigned .map ((a) => a[g])))
这取决于一个 mean
函数,我们将把它连同它所需的 sum
函数一起编写,如下所示:
const sum = (ns) => ns .reduce ((t, n) => t + n, 0)
const mean = xs => sum (xs) / xs.length
然后我们需要更新movement
。我们可以根据 centroids
和 newCentroids
:
轻松做到这一点
movement = centroids.some((c, i) => c !== newCentroids[i])
显然,您可以继续这种方式。每个 for
循环都应该有一个基本目的。找到那个目的,看看 Array.prototype
方法之一是否可以更好地表达它。对于我们上面处理的第二部分,我们发现了两个目的,并将它们分成两个单独的块。
这应该会给您一个良好的开端,使它更加实用。没有灵丹妙药。但是,如果您根据不可变数据的纯函数以及关注点的强分离来考虑,您通常可以朝函数方向移动。
我在 javascript 中有一个非常基本的 k-means 实现(我知道,但它需要在浏览器中 运行)。我想了解的是 - 如何使它更具功能性?
它目前充满了循环,并且极难理解/推理,代码如下:
export default class KMeans {
constructor(vectors, k) {
this.vectors = vectors;
this.numOfVectors = vectors.length;
this.k = k || bestGuessK(this.numOfVectors);
this.centroids = randomCentroids(this.vectors, this.k);
}
classify(vector, distance) {
let min = Infinity;
let index = 0;
for (let i = 0; i < this.centroids.length; i++) {
const dist = distance(vector, this.centroids[i]);
if (dist < min) {
min = dist;
index = i;
}
}
return index;
}
cluster() {
const assigment = new Array(this.numOfVectors);
const clusters = new Array(this.k);
let movement = true;
while (movement) {
// update vector to centroid assignments
for (let i = 0; i < this.numOfVectors; i++) {
assigment[i] = this.classify(this.vectors[i], euclidean);
}
// update location of each centroid
movement = false;
for (let j = 0; j < this.k; j++) {
const assigned = [];
for (let i = 0; i < assigment.length; i++) {
if (assigment[i] === j) assigned.push(this.vectors[i]);
}
if (!assigned.length) continue;
const centroid = this.centroids[j];
const newCentroid = new Array(centroid.length);
for (let g = 0; g < centroid.length; g++) {
let sum = 0;
for (let i = 0; i < assigned.length; i++) {
sum += assigned[i][g];
}
newCentroid[g] = sum / assigned.length;
if (newCentroid[g] !== centroid[g]) {
movement = true;
}
}
this.centroids[j] = newCentroid;
clusters[j] = assigned;
}
}
return clusters;
}
}
当然可以。
你可以从这个开始:
classify(vector, distance) {
let min = Infinity;
let index = 0;
for (let i = 0; i < this.centroids.length; i++) {
const dist = distance(vector, this.centroids[i]);
if (dist < min) {
min = dist;
index = i;
}
}
return index;
}
为什么这是一个成员函数?纯函数 const classify = (centroids, vector, distance) => {...}
不是更干净吗?
然后为了实现,让我们稍微更改一下 distance
签名。如果我们将它柯里化为 const distance = (vector) => (centroid) => {...}
,我们就可以写
const classify = (centroids, vector, distance) =>
minIndex (centroids .map (distance (vector)))
如果 distance
API 不在我们的控制范围内,那就更难了:
const classify = (centroids, vector, distance) =>
minIndex (centroids .map (centroid => distance (vector, centroid)))
当然,我们还没有写 minIndex
,但我们已经分解了问题以使用更有意义的抽象。 minIndex
并不难写。你可以像原来的 classify
函数那样强制执行,或者像这样:
const minIndex = (xs) => xs.indexOf (Math.min (...xs))
请注意,distance
在这里是一个有点误导的名称。我不得不更仔细地阅读它,因为我假设这样的名字代表……,好吧,一段距离。相反,它是一个用于计算距离的函数。也许名称 metric
或类似 distanceFunction
、distanceFn
或 distanceImpl
的名称会更明显。
现在让我们继续这一点:
const newCentroid = new Array(centroid.length);
for (let g = 0; g < centroid.length; g++) {
let sum = 0;
for (let i = 0; i < assigned.length; i++) {
sum += assigned[i][g];
}
newCentroid[g] = sum / assigned.length;
if (newCentroid[g] !== centroid[g]) {
movement = true;
}
}
此代码有两个职责:创建 newCentroid
数组,并在任何值更改时更新 movement
的值。
让我们把这两个分开。
首先,创建新的质心。我们可以将嵌套的 for
-loop 清理成这样:
const makeNewCentroid = (centroid, assigned) =>
centroid .map ((c, g) => mean (assigned .map ((a) => a[g])))
这取决于一个 mean
函数,我们将把它连同它所需的 sum
函数一起编写,如下所示:
const sum = (ns) => ns .reduce ((t, n) => t + n, 0)
const mean = xs => sum (xs) / xs.length
然后我们需要更新movement
。我们可以根据 centroids
和 newCentroids
:
movement = centroids.some((c, i) => c !== newCentroids[i])
显然,您可以继续这种方式。每个 for
循环都应该有一个基本目的。找到那个目的,看看 Array.prototype
方法之一是否可以更好地表达它。对于我们上面处理的第二部分,我们发现了两个目的,并将它们分成两个单独的块。
这应该会给您一个良好的开端,使它更加实用。没有灵丹妙药。但是,如果您根据不可变数据的纯函数以及关注点的强分离来考虑,您通常可以朝函数方向移动。