From eab997898f4801cb45c7864b292a69a69b01fa9c Mon Sep 17 00:00:00 2001 From: Frank Xu Date: Tue, 12 Oct 2021 01:37:43 -0400 Subject: [PATCH 1/5] fix Cosine_FaissHNSW --- graphgrove/graph_builder.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/graphgrove/graph_builder.py b/graphgrove/graph_builder.py index 34c7d0e..c67daaa 100644 --- a/graphgrove/graph_builder.py +++ b/graphgrove/graph_builder.py @@ -14,6 +14,7 @@ limitations under the License. """ + from absl import logging import numpy as np from scipy.sparse import coo_matrix @@ -256,15 +257,14 @@ def topk(self, query): return results[0].astype(np.float32), results[1].astype(np.int32) class Cosine_FaissHNSW(Index): - def __init__(self, k, max_degree=128, efSearch=128, efConstruction=200, add_noise=True, noise_amount=1e-6, - assume_unit_norm=True): - super(FaissHNSW, self).__init__(k) + assume_unit_normed=True): + super(Cosine_FaissHNSW, self).__init__(k) self.index = None self.row = None self.col = None @@ -276,7 +276,7 @@ def __init__(self, k, self.efSearch = efSearch self.efConstruction = efConstruction self.noise_amount = noise_amount - self.assume_unit_norm = assume_unit_norm + self.assume_unit_normed = assume_unit_normed def build(self, vectors): t0 = time.time() @@ -285,6 +285,7 @@ def build(self, vectors): self.num_points += vectors.shape[0] if not self.assume_unit_normed: vectors = unit_norm(vectors) + import faiss self.index = faiss.IndexHNSWFlat(vectors.shape[1], self.max_degree) self.index.hnsw.efConstruction = self.efConstruction self.index.hnsw.efSearch = self.efSearch From f2cde9b2a127ade511bb0b5a79ede7fced2592a9 Mon Sep 17 00:00:00 2001 From: Frank Xu Date: Fri, 15 Oct 2021 02:19:02 -0400 Subject: [PATCH 2/5] add distribution --- examples/cluster.py | 46 ++++++++++++ examples/clustering.py | 70 ------------------ examples/dag_clustering.py | 45 ------------ examples/nearest_neighbor_search.py | 87 ----------------------- examples/plot_cluster.py | 52 ++++++++++++++ examples/plot_cluster_num_distribution.py | 46 ++++++++++++ graphgrove/graph_builder.py | 2 +- graphgrove/vec_scc.py | 2 - 8 files changed, 145 insertions(+), 205 deletions(-) create mode 100644 examples/cluster.py delete mode 100644 examples/clustering.py delete mode 100644 examples/dag_clustering.py delete mode 100644 examples/nearest_neighbor_search.py create mode 100644 examples/plot_cluster.py create mode 100644 examples/plot_cluster_num_distribution.py diff --git a/examples/cluster.py b/examples/cluster.py new file mode 100644 index 0000000..0c6c062 --- /dev/null +++ b/examples/cluster.py @@ -0,0 +1,46 @@ +import time +import sys + +import numpy as np +import matplotlib.pyplot as plt +import tqdm +import torch + +from graphgrove.vec_scc import Cosine_SCC +from graphgrove.graph_builder import unit_norm + +gt = time.time + +np.random.seed(123) +cores = 80 + +num_rounds = 50 +thresholds = np.geomspace(1.0, 0.001, num_rounds).astype(np.float32) + +for idx in tqdm.tqdm(range(50, 10000)): + vectors = np.load('../knnlm-distill/dstore/ids/' + str(idx) + '.npy') + print(vectors.shape) + vectors = unit_norm(vectors) + vectors = vectors.astype(np.float32) + + t = gt() + scc = Cosine_SCC(k=25, num_rounds=num_rounds, thresholds=thresholds, + index_name='cosine_faisshnsw', cores=cores, verbosity=0) + scc.partial_fit(vectors) + b_t = gt() - t + scc = scc.scc + # cos_scc operates on vectors, it's member object, scc + # (https://github.com/nmonath/graphgrove/blob/main/graphgrove/scc.py) operates on the k-nearest neighbor graph. + levels = scc.levels # clustering will store the flat clustering + + cluster_data_save = {'thresholds': thresholds, + 'cluster_levels': []} + + for selected_level in range(num_rounds + 1): + clustering = [] + for node in levels[selected_level].nodes: + clustering.append( + node.descendants().squeeze(-1)) # descendants has the ids of the data points which are the descendant leaves + cluster_data_save['cluster_levels'].append(clustering) + + torch.save(cluster_data_save, '../knnlm-distill/dstore/clusters/' + str(idx) + '.pt') \ No newline at end of file diff --git a/examples/clustering.py b/examples/clustering.py deleted file mode 100644 index 2dda7b8..0000000 --- a/examples/clustering.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Copyright (c) 2021 The authors of SCC All rights reserved. - -Initially modified from CoverTree -https://github.com/manzilzaheer/CoverTree -Copyright (c) 2017 Manzil Zaheer All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -import time -import sys - -import numpy as np - -from graphgrove.vec_scc import Cosine_SCC -from graphgrove.graph_builder import unit_norm - -gt = time.time - -np.random.seed(123) -cores = 1 - -print('======== Building Dataset ==========') -N=100 -K=5 -D=784 -means = 20*np.random.rand(K,D) - 10 -x = np.vstack([np.random.randn(N,D) + means[i] for i in range(K)]) -np.random.shuffle(x) -x = unit_norm(x) -x = x.astype(np.float32) -x = np.require(x, requirements=['A', 'C', 'O', 'W']) -y = np.vstack([np.random.randn(N,D) + means[i] for i in range(K)]) -y = y.astype(np.float32) -y = np.require(y, requirements=['A', 'C', 'O', 'W']) - -print('======== SCC ==========') -t = gt() -num_rounds = 50 -thresholds = np.geomspace(1.0, 0.001, num_rounds).astype(np.float32) -scc = Cosine_SCC(k=5, num_rounds=num_rounds, thresholds=thresholds, index_name='cosine_sgtree', cores=cores, verbosity=1) -scc.partial_fit(x) -b_t = gt() - t -print("Clustering time:", b_t, "seconds") -sys.stdout.flush() - -print('======== MB-SCC ==========') -t = gt() -num_rounds = 50 -thresholds = np.geomspace(1.0, 0.001, num_rounds).astype(np.float32) -scc = Cosine_SCC(k=5, num_rounds=num_rounds, thresholds=thresholds, index_name='cosine_sgtree', cores=cores, verbosity=0) -bs = 1 -for i in range(0, x.shape[0], bs): - # print(i) - scc.partial_fit(x[i:i+bs]) -b_t = gt() - t -print("Clustering time:", b_t, "seconds") -del scc -sys.stdout.flush() diff --git a/examples/dag_clustering.py b/examples/dag_clustering.py deleted file mode 100644 index 289a114..0000000 --- a/examples/dag_clustering.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Copyright (c) 2021 The authors of LLama All rights reserved. - -Initially modified from CoverTree -https://github.com/manzilzaheer/CoverTree -Copyright (c) 2017 Manzil Zaheer All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -from graphgrove.llama import LLAMA -import numpy as np -from scipy.sparse import coo_matrix - -num_nodes = 100 -num_edges = 2500 -r = np.random.choice(num_nodes, size=num_edges) -c = np.random.choice(num_nodes, size=num_edges) -sim = np.random.random_sample(size=num_edges).astype(np.float32) -graph = coo_matrix((sim,(r,c))) - -def make_symmetric(coo_mat): - lil = coo_mat.tolil() - rows, cols = lil.nonzero() - lil[cols, rows] = lil[rows, cols].maximum(lil[cols, rows]) - return lil.tocoo() - -graph = make_symmetric(graph) - -llama = LLAMA.from_graph(graph, num_rounds=10, cores=3, linkage='approx_average') -llama.cluster() - -print(llama.assignments()) -print(llama.structure()) -print(llama.round(2)) \ No newline at end of file diff --git a/examples/nearest_neighbor_search.py b/examples/nearest_neighbor_search.py deleted file mode 100644 index 7ffaad1..0000000 --- a/examples/nearest_neighbor_search.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Copyright (c) 2021 The authors of SG Tree All rights reserved. - -Initially modified from CoverTree -https://github.com/manzilzaheer/CoverTree -Copyright (c) 2017 Manzil Zaheer All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -import time -import numpy as np - -from graphgrove.sgtree import NNS_L2 as SGTree_NNS_L2 -from graphgrove.covertree import NNS_L2 as CoverTree_NNS_L2 - -gt = time.time - -np.random.seed(123) -cores = 4 - -print('======== Building Dataset ==========') -N=1000 -K=10 -D=784 -means = 20*np.random.rand(K,D) - 10 -x = np.vstack([np.random.randn(N,D) + means[i] for i in range(K)]) -np.random.shuffle(x) -x = x.astype(np.float32) -x = np.require(x, requirements=['A', 'C', 'O', 'W']) -y = np.vstack([np.random.randn(N,D) + means[i] for i in range(K)]) -y = y.astype(np.float32) -y = np.require(y, requirements=['A', 'C', 'O', 'W']) - -print('======== Cover Tree ==========') -t = gt() -ct = CoverTree_NNS_L2.from_matrix(x, use_multi_core=cores) -b_t = gt() - t -#ct.display() -print("Building time:", b_t, "seconds") - -print('Test k-Nearest Neighbours - Exact (k=3): ') -t = gt() -idx1, d1 = ct.kNearestNeighbours(y,3, use_multi_core=cores) -b_t = gt() - t -print("Query time - Exact:", b_t, "seconds") - -print('======== SG Tree ==========') -t = gt() -ct = SGTree_NNS_L2.from_matrix(x, use_multi_core=cores) -b_t = gt() - t -#ct.display() -print("Building time:", b_t, "seconds") - -print('Test k-Nearest Neighbours - Exact (k=3): ') -t = gt() -idx1, d1 = ct.kNearestNeighbours(y,3, use_multi_core=cores) -b_t = gt() - t -print("Query time - Exact:", b_t, "seconds") - -print('Test k-Nearest Neighbours - Beam (k=3, beam_size=10): ') -t = gt() -idx1, d1 = ct.kNearestNeighboursBeam(y, 3, use_multi_core=cores, beam_size=10) -b_t = gt() - t -print("Query time - Beam:", b_t, "seconds") - -print('Test Range - cores=0') -t = gt() -idx1, d1 = ct.RangeSearch(y, r=0.5, use_multi_core=0) -b_t = gt() - t -print("Query time - Range:", b_t, "seconds") - -print('Test Range - cores=%s' % cores) -t = gt() -idx1, d1 = ct.RangeSearch(y, r=0.5, use_multi_core=cores) -b_t = gt() - t -print("Query time - Range:", b_t, "seconds") \ No newline at end of file diff --git a/examples/plot_cluster.py b/examples/plot_cluster.py new file mode 100644 index 0000000..5aceea2 --- /dev/null +++ b/examples/plot_cluster.py @@ -0,0 +1,52 @@ +import time +import sys + +import numpy as np +import matplotlib.pyplot as plt +import tqdm + +from graphgrove.vec_scc import Cosine_SCC +from graphgrove.graph_builder import unit_norm +from fairseq.data import Dictionary +dictionary = Dictionary.load('../knnlm-distill/data-bin/wikitext103-bpe/dict.txt') + +to_save = ['bank', 'shore', 'institution', 'beautiful'] + +gt = time.time + +np.random.seed(123) +cores = 80 + +for word in to_save: + plot_x = [] + plot_y = [] + + idx = dictionary.index(word) + print(idx) + vectors = np.load('../knnlm-distill/dstore/ids/' + str(idx) + '.npy') + print(vectors.shape) + vectors = unit_norm(vectors) + vectors = vectors.astype(np.float32) + t = gt() + num_rounds = 50 + thresholds = np.geomspace(1.0, 0.001, num_rounds).astype(np.float32) + scc = Cosine_SCC(k=25, num_rounds=num_rounds, thresholds=thresholds, + index_name='cosine_faisshnsw', cores=cores, verbosity=0) + scc.partial_fit(vectors) + b_t = gt() - t + scc = scc.scc # cos_scc operates on vectors, it's member object, scc (https://github.com/nmonath/graphgrove/blob/main/graphgrove/scc.py) operates on the k-nearest neighbor graph. + levels = scc.levels # clustering will store the flat clustering + for selected_level in range(num_rounds + 1): + clustering = [] + for node in levels[selected_level].nodes: + clustering.append( + node.descendants()) # descendants has the ids of the data points which are the descendant leaves + number_clusters = len(clustering) + print(selected_level, number_clusters) + plot_x.append(selected_level) + plot_y.append(number_clusters) + + plt.scatter(plot_x, plot_y, label=word, s=4) +plt.legend(loc="upper right") +plt.ylim([0, 1000]) +plt.savefig('words.pdf') diff --git a/examples/plot_cluster_num_distribution.py b/examples/plot_cluster_num_distribution.py new file mode 100644 index 0000000..415252c --- /dev/null +++ b/examples/plot_cluster_num_distribution.py @@ -0,0 +1,46 @@ +import time +import sys + +import numpy as np +import matplotlib.pyplot as plt +import tqdm +import torch + +from graphgrove.vec_scc import Cosine_SCC +from graphgrove.graph_builder import unit_norm + +gt = time.time + +np.random.seed(123) +cores = 80 + +num_rounds = 50 +thresholds = np.geomspace(1.0, 0.001, num_rounds).astype(np.float32) +print(len(thresholds)) +plot_x = [] +plot_y = [] + +plot_detail_x = [] +plot_detail_y = [] + +for idx in tqdm.tqdm(range(50, 10000, 50)): + cluster_result = torch.load('../knnlm-distill/dstore/clusters/' + str(idx) + '.pt') + for level, cluster in enumerate(cluster_result['cluster_levels']): + # if level > 0: + # plot_x.append(thresholds[level-1]) + # plot_y.append(len(cluster)) + if level > 40: + plot_detail_x.append(thresholds[level-1]) + plot_detail_y.append(len(cluster)) + +# plt.scatter(plot_x, plot_y, s=3) +# plt.savefig('full_dist.pdf') +# +# plt.clf() + + +plt.scatter(plot_detail_x, plot_detail_y, s=3) +plt.savefig('right_dist_smpl.pdf') + +plt.clf() + diff --git a/graphgrove/graph_builder.py b/graphgrove/graph_builder.py index c67daaa..3bbdd1c 100644 --- a/graphgrove/graph_builder.py +++ b/graphgrove/graph_builder.py @@ -327,5 +327,5 @@ def topk(self, query): if not self.assume_unit_normed: query = unit_norm(query) results = self.index.search(query, min(self.k, self.num_points)) - return (2-results[0].astype(np.float32)**2)/2, results[1].astype(np.int32) + return (2-results[0].astype(np.float32))/2, results[1].astype(np.int32) diff --git a/graphgrove/vec_scc.py b/graphgrove/vec_scc.py index fde13fa..f053573 100644 --- a/graphgrove/vec_scc.py +++ b/graphgrove/vec_scc.py @@ -42,7 +42,6 @@ def __init__(self, k=25, num_rounds=50, thresholds=None, index_name='cosine_sgtr self.hnsw_max_degree = hnsw_max_degree self.hnsw_ef_search = hnsw_ef_search self.hnsw_ef_construction = hnsw_ef_construction - if self.index_name.lower() == 'cosine_covertree': self.index = graph_builder.Cosine_CoverTree(self.k, self.cores) elif self.index_name.lower() == 'cosine_sgtree': @@ -53,7 +52,6 @@ def __init__(self, k=25, num_rounds=50, thresholds=None, index_name='cosine_sgtr self.index = graph_builder.Cosine_FaissFlat(self.k) elif self.index_name.lower() == 'cosine_faisshnsw': self.index = graph_builder.Cosine_FaissHNSW(self.k, self.hnsw_max_degree, self.hnsw_ef_search, self.hnsw_ef_construction) - self.scc = SCC.init(self.thresholds, self.cores, self.cc_alg, self.par_minimum, self.verbosity) # def __del__(self): From f8e1eb8afcaf34a2414073d21638558938a7c84a Mon Sep 17 00:00:00 2001 From: Frank Xu Date: Wed, 20 Oct 2021 21:07:56 -0400 Subject: [PATCH 3/5] add distribution --- examples/plot_cluster_num_distribution.py | 54 +++++++++++++---------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/examples/plot_cluster_num_distribution.py b/examples/plot_cluster_num_distribution.py index 415252c..7a73d61 100644 --- a/examples/plot_cluster_num_distribution.py +++ b/examples/plot_cluster_num_distribution.py @@ -1,14 +1,10 @@ import time -import sys -import numpy as np import matplotlib.pyplot as plt -import tqdm +import numpy as np import torch - -from graphgrove.vec_scc import Cosine_SCC -from graphgrove.graph_builder import unit_norm - +import tqdm +from multiprocessing import Pool gt = time.time np.random.seed(123) @@ -23,24 +19,36 @@ plot_detail_x = [] plot_detail_y = [] -for idx in tqdm.tqdm(range(50, 10000, 50)): - cluster_result = torch.load('../knnlm-distill/dstore/clusters/' + str(idx) + '.pt') - for level, cluster in enumerate(cluster_result['cluster_levels']): - # if level > 0: - # plot_x.append(thresholds[level-1]) - # plot_y.append(len(cluster)) - if level > 40: - plot_detail_x.append(thresholds[level-1]) - plot_detail_y.append(len(cluster)) - -# plt.scatter(plot_x, plot_y, s=3) -# plt.savefig('full_dist.pdf') -# -# plt.clf() +levels_interested = [40, 45, 50] +histo_data = dict.fromkeys(levels_interested) +for x in histo_data: + histo_data[x] = [] +# def process_cluster_id(cluster_id): +# cluster_result = torch.load('../knnlm-distill/dstore/clusters/' + str(cluster_id) + '.pt') +# for level, cluster in enumerate(cluster_result['cluster_levels']): +# if level in levels_interested: +# histo_data[level].append(len(cluster)) +# +# with Pool(processes=60) as pool: +bins = np.linspace(0, 40, 20) +print(bins) -plt.scatter(plot_detail_x, plot_detail_y, s=3) -plt.savefig('right_dist_smpl.pdf') +for idx in tqdm.tqdm(range(50, 10000, 10)): + cluster_result = torch.load('../knnlm-distill/dstore/clusters/' + str(idx) + '.pt') + for level, cluster in enumerate(cluster_result['cluster_levels']): + if level in levels_interested: + num_cls = len(cluster) + if num_cls > 40: + num_cls = 40 + histo_data[level].append(num_cls) + +for l in histo_data: + print(np.sum(histo_data[l])*10) + plt.hist(histo_data[l], bins, alpha=0.5, label=thresholds[l-1]) +plt.legend(loc='upper right') +# plt.ylim(0, 1000) +plt.savefig('histogram_num_clusters.pdf') plt.clf() From 3fe298587e40f38e71197ce96455a14291048a25 Mon Sep 17 00:00:00 2001 From: Frank Xu Date: Wed, 20 Oct 2021 21:08:42 -0400 Subject: [PATCH 4/5] add plots --- histogram_num_clusters.pdf | Bin 0 -> 8425 bytes words.pdf | Bin 0 -> 13081 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 histogram_num_clusters.pdf create mode 100644 words.pdf diff --git a/histogram_num_clusters.pdf b/histogram_num_clusters.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dcc1007dfab4dc6a365b2b0dd63a290a3b89e160 GIT binary patch literal 8425 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 Date: Wed, 20 Oct 2021 21:09:35 -0400 Subject: [PATCH 5/5] add plots --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b6e4761..c5c1289 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ __pycache__/ *.py[cod] *$py.class +.idea/ # C extensions *.so