我在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
循环清理为类似以下内容:
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
方法之一是否可以更好地表达它。对于上面我们工作的第二部分,我们发现了两个目的,并将它们分为两个单独的块。
这应该使您有一个更好的开始。没有魔术子弹。但是,如果您考虑不可变数据的纯功能以及关注点之间的强烈分离,通常可以朝功能方向发展。