Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KMeans 중심값 생성 함수 추가 (BallCut) #37

Merged
merged 17 commits into from
Nov 28, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 15 additions & 3 deletions Project17-C-Map/InteractiveClusteringMap.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
FAC9FD05256E2107009FBB41 /* KCoefficient.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC9FD04256E2107009FBB41 /* KCoefficient.swift */; };
FAC9FD0A256E23B1009FBB41 /* KDefineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC9FD09256E23B1009FBB41 /* KDefineTests.swift */; };
FAC9FD0E256E2495009FBB41 /* KCoefficient.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC9FD04256E2107009FBB41 /* KCoefficient.swift */; };
FC9F5C32257094A20029A53B /* KMeansPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC9F5C31257094A20029A53B /* KMeansPerformanceTests.swift */; };
FCE422B02570194B0017416F /* Coordinate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE422AF2570194B0017416F /* Coordinate.swift */; };
FCE422B8257019650017416F /* Cluster.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE422B7257019650017416F /* Cluster.swift */; };
FCE422BD25701A200017416F /* Quadrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE422BC25701A200017416F /* Quadrant.swift */; };
Expand Down Expand Up @@ -120,6 +121,7 @@
FA81D67725690F360023E123 /* CoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = "<group>"; };
FAC9FD04256E2107009FBB41 /* KCoefficient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCoefficient.swift; sourceTree = "<group>"; };
FAC9FD09256E23B1009FBB41 /* KDefineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KDefineTests.swift; sourceTree = "<group>"; };
FC9F5C31257094A20029A53B /* KMeansPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMeansPerformanceTests.swift; sourceTree = "<group>"; };
FCE422AF2570194B0017416F /* Coordinate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinate.swift; sourceTree = "<group>"; };
FCE422B7257019650017416F /* Cluster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cluster.swift; sourceTree = "<group>"; };
FCE422BC25701A200017416F /* Quadrant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quadrant.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -229,13 +231,11 @@
37EB73412562592900AC44D6 /* InteractiveClusteringMapTests */ = {
isa = PBXGroup;
children = (
FC9F5C30257094710029A53B /* KMeans */,
46A12E3E256CEDD3007BDF3E /* QuadTreeTests */,
37EB73422562592900AC44D6 /* InteractiveClusteringMapTests.swift */,
37279B02256EBC5200EA689A /* KMeansTests.swift */,
3713D3DB25694C3E00E703EC /* CoreDataStackTests.swift */,
FAC9FD09256E23B1009FBB41 /* KDefineTests.swift */,
37EB73442562592900AC44D6 /* Info.plist */,
FCE422C625701B8A0017416F /* KMeansCentroidsTest.swift */,
);
path = InteractiveClusteringMapTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -304,6 +304,17 @@
path = POI;
sourceTree = "<group>";
};
FC9F5C30257094710029A53B /* KMeans */ = {
isa = PBXGroup;
children = (
FCE422C625701B8A0017416F /* KMeansCentroidsTest.swift */,
FAC9FD09256E23B1009FBB41 /* KDefineTests.swift */,
37279B02256EBC5200EA689A /* KMeansTests.swift */,
FC9F5C31257094A20029A53B /* KMeansPerformanceTests.swift */,
);
path = KMeans;
sourceTree = "<group>";
};
FCE422AD257018E30017416F /* Clustering */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -560,6 +571,7 @@
3713D3E025694C6B00E703EC /* CoreDataStack.swift in Sources */,
3731108C256F641F00350D55 /* POIService.swift in Sources */,
FCE422D625701CBD0017416F /* Quadrant.swift in Sources */,
FC9F5C32257094A20029A53B /* KMeansPerformanceTests.swift in Sources */,
46A12E40256CEDF2007BDF3E /* BoundingBoxTests.swift in Sources */,
192D34B6256E36A000861703 /* QuadTree.swift in Sources */,
46A12E2F256CED3A007BDF3E /* AppDelegate.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,89 @@

import Foundation

class KMeans {
final class KMeans {

private let k: Int

init(k: Int) {
self.k = k
}

func randomCentroids(rangeOfLat lats: ClosedRange<Double>,
func initializeBallCutCentroids(k: Int,
coverage: Double,
coordinates: [Coordinate]) -> [Coordinate] {
var coordinates = coordinates
var centroids: [Coordinate] = []
let container = createContainer(k: k, coordinates: coordinates)
centroids += pickCentroids(k: k, distance: coverage, container: container)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit : var centroids = pickCentroids(k: k, distance: coverage, container: container)
이렇게 하는건 어떤가용 ??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋아요!

if centroids.count != k {
for coordinate in container {
guard let index = coordinates.firstIndex(of: coordinate) else {
return []
}
coordinates.remove(at: index)
}

centroids += initializeBallCutCentroids(k: k - centroids.count + 1, coverage: coverage, coordinates: coordinates)
}

return centroids
}

private func createContainer(k: Int, coordinates: [Coordinate], mutiplier: Int = 5) -> [Coordinate] {
var coordinates = coordinates
var container: [Coordinate] = []
let count = k * mutiplier
for _ in 0..<count {
guard let coordinate = coordinates.randomElement(),
let index = coordinates.firstIndex(of: coordinate)
else {
return []
}
container.append(coordinate)
coordinates.remove(at: index)
}

return container
}

private func pickCentroids(k: Int, distance: Double, container: [Coordinate]) -> [Coordinate] {
var centroids: [Coordinate] = []
var container = container

for _ in 0..<k {
guard let centroid = container.randomElement() else {
return centroids
}

var remove: [Coordinate] = []
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 변수명 바꿔줘야 할 것 같아요!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indexForRemoveCentroids 어떨까요...?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removeCoordinates로 수정했습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indexForRemoveCentroids 어떨까요...?

index를 담는 배열이 아니에요!!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅎㅎㅎ 죄송합니당 잘못봤네요


container.forEach { coordinate in
if centroid.distanceTo(coordinate) <= distance {
remove.append(coordinate)
}
}

container = container.filter { !remove.contains($0) }
centroids.append(centroid)
}

return centroids
}

func initializeRandomCentroids(rangeOfLat lats: ClosedRange<Double>,
rangeOfLng lngs: ClosedRange<Double>) -> [Coordinate] {
var centroids: [Coordinate] = []
for _ in 0..<k {
let lat = Double.random(in: lats)
let lng = Double.random(in: lngs)
let cluster = Coordinate(x: lng, y: lat)
let cluster = Coordinate.randomGenerate(rangeOfLat: lats, rangeOfLng: lngs)
centroids.append(cluster)
}

return centroids
}

func screenCentroids(topLeft: Coordinate, bottomRight: Coordinate) -> [Coordinate] {
func initializeScreenCentroids(topLeft: Coordinate, bottomRight: Coordinate) -> [Coordinate] {
let center = (topLeft + bottomRight) / 2.0
let boundary = Coordinate(x: bottomRight.x - center.x, y: topLeft.y - center.y)
let pivot = center.findTheta(vertex: Coordinate(x: bottomRight.x, y: topLeft.y))
Expand All @@ -50,7 +111,7 @@ class KMeans {
let radian = theta * .pi / Degree.straight
var x: Double
var y: Double

if theta > pivot {
y = boundary.y
x = y / tan(radian)
Expand Down
12 changes: 12 additions & 0 deletions Project17-C-Map/InteractiveClusteringMap/VO/Coordinate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ struct Coordinate {

}

extension Coordinate {

static func randomGenerate(rangeOfLat lats: ClosedRange<Double>,
rangeOfLng lngs: ClosedRange<Double>) -> Coordinate {
let lat = Double.random(in: lats)
let lng = Double.random(in: lngs)

return Coordinate(x: lng, y: lat)
}

}

extension Coordinate: Hashable {

static let zero = Coordinate(x: 0, y: 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,26 @@ class KMeansCentroidsTest: XCTestCase {
}

func test_coordinates_by_random() throws {
let cendroids = mockCentoidsByRandom(number: 1000)
let centroids = mockCentoidsByRandom(number: 1000)

cendroids.forEach { cendroid in
validateCendtoid(cendroid: cendroid)
centroids.forEach { cendroid in
validateCentroids(cendroid: cendroid)
}
}

func test_centroids_by_coverage_distance() throws {
let centroids = mockCentoidsByRandom(number: 1000)
let kmm = KMeans(k: 5)

let minX = 126.9903617
let maxX = 126.9956437
let distance = maxX - minX
let coverage = distance / 3

let result = kmm.initializeBallCutCentroids(k: 13, coverage: coverage, coordinates: centroids)
print(result)
}

private func validateCendroids(centroids: [Coordinate], expected: [Coordinate]) {
XCTAssertEqual(centroids.count, expected.count)

Expand All @@ -78,7 +91,7 @@ class KMeansCentroidsTest: XCTestCase {
}
}

private func validateCendtoid(cendroid: Coordinate) {
private func validateCentroids(cendroid: Coordinate) {
XCTAssert((33.0...43.0).contains(cendroid.y))
XCTAssert((123.0...132.0).contains(cendroid.x))
}
Expand All @@ -87,13 +100,13 @@ class KMeansCentroidsTest: XCTestCase {
let kmm = KMeans(k: number)
let topLeft = Coordinate(x: 124.0, y: 43.0)
let bottomRight = Coordinate(x: 132.0, y: 33.0)
return kmm.screenCentroids(topLeft: topLeft, bottomRight: bottomRight)
return kmm.initializeScreenCentroids(topLeft: topLeft, bottomRight: bottomRight)
}

private func mockCentoidsByRandom(number: Int) -> [Coordinate] {
let kmm = KMeans(k: number)

return kmm.randomCentroids(rangeOfLat: 33.0...43.0, rangeOfLng: 123.0...132.0)
return kmm.initializeRandomCentroids(rangeOfLat: 33.0...43.0, rangeOfLng: 123.0...132.0)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// KMeansPerformanceTests.swift
// InteractiveClusteringMapTests
//
// Created by Seungeon Kim on 2020/11/27.
//

import XCTest

fileprivate struct Mock {
static let minX = 126.9903617
static let maxX = 126.9956437
static let minY = 37.5600365
static let maxY = 37.5764792
}

class KMeansPerformanceTests: XCTestCase {
let group = DispatchGroup()
lazy var coords: [Coordinate] = { () -> [Coordinate] in
var points: [Coordinate] = []
for _ in 0..<10000 {
points.append(generatePoint())
}
return points
}()

func test_performance_measure_kmeans() throws {
measure {
kmeansByScreen(k: 22, coords: coords)
}
}

func test_performance_measure_kmeans_by_random() throws {
measure {
kmeansByRandom(k: 22, coords: coords)
}
}

private func asyncNotify(coords: [Coordinate], compltion handler: @escaping ([Coordinate]) -> Void) {
group.notify(queue: DispatchQueue.main) {
handler(coords)
}
}

private func generatePoint() -> Coordinate {
let lat = Double.random(in: 37.5600365...37.5764792)
let lng = Double.random(in: 126.9903617...126.9956437)

return Coordinate(x: lng, y: lat)
}

@discardableResult
private func kmeansByScreen(k: Int, coords: [Coordinate]) -> [Cluster] {
let kmm = KMeans(k: 22)

let topLeft = Coordinate(x: Mock.minX, y: Mock.maxY)
let bottomRight = Coordinate(x: Mock.maxX, y: Mock.minY)
let centroids = kmm.screenCentroids(topLeft: topLeft, bottomRight: bottomRight)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메서드 명이 바뀐 거를 깜빡 하셨나 보네요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵.. 바로 수정했습니다! 😅

var clusters: [Cluster] = []

clusters = kmm.trainCenters(coords, initialCentroids: centroids)

return clusters
}

@discardableResult
private func kmeansByRandom(k: Int, coords: [Coordinate]) -> [Cluster] {
let kmm = KMeans(k: 22)
let centroids = kmm.randomCentroids(rangeOfLat: Mock.minY...Mock.maxY,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도요.

rangeOfLng: Mock.minX...Mock.maxX)
var clusters: [Cluster] = []

clusters = kmm.trainCenters(coords, initialCentroids: centroids)

return clusters
}

private func timeout(_ timeout: TimeInterval, completion: (XCTestExpectation) throws -> Void) rethrows {
let exp = expectation(description: "Timeout: \(timeout) seconds")

try completion(exp)

waitForExpectations(timeout: timeout) { error in
guard let error = error else { return }
XCTFail("Timeout error: \(error)")
}
}
}