2011-06-07 00:40:00 +08:00
|
|
|
"""
|
2011-12-21 20:09:06 +08:00
|
|
|
=======================================
|
|
|
|
|
Clustering text documents using k-means
|
|
|
|
|
=======================================
|
2011-06-07 00:40:00 +08:00
|
|
|
|
|
|
|
|
This is an example showing how the scikit-learn can be used to cluster
|
|
|
|
|
documents by topics using a bag-of-words approach. This example uses
|
|
|
|
|
a scipy.sparse matrix to store the features instead of standard numpy arrays.
|
|
|
|
|
|
2012-12-15 01:53:20 +08:00
|
|
|
Two feature extraction methods can be used in this example:
|
|
|
|
|
|
|
|
|
|
- TfidfVectorizer uses a in-memory vocabulary (a python dict) to map the most
|
|
|
|
|
frequent words to features indices and hence compute a word occurrence
|
|
|
|
|
frequency (sparse) matrix. The word frequencies are then reweighted using
|
|
|
|
|
the Inverse Document Frequency (IDF) vector collected feature-wise over
|
|
|
|
|
the corpus.
|
|
|
|
|
|
|
|
|
|
- HashingVectorizer hashes word occurrences to a fixed dimensional space,
|
2013-01-20 21:55:15 +08:00
|
|
|
possibly with collisions. The word count vectors are then normalized to
|
|
|
|
|
each have l2-norm equal to one (projected to the euclidean unit-ball) which
|
|
|
|
|
seems to be important for k-means to work in high dimensional space.
|
2012-12-15 01:53:20 +08:00
|
|
|
|
|
|
|
|
HashingVectorizer does not provide IDF weighting as this is a stateless
|
2012-12-16 00:48:17 +08:00
|
|
|
model (the fit method does nothing). When IDF weighting is needed it can
|
|
|
|
|
be added by pipelining its output to a TfidfTransformer instance.
|
2012-12-15 01:53:20 +08:00
|
|
|
|
|
|
|
|
Two algorithms are demoed: ordinary k-means and its more scalable cousin
|
|
|
|
|
minibatch k-means.
|
|
|
|
|
|
2017-12-13 20:02:39 +08:00
|
|
|
Additionally, latent semantic analysis can also be used to reduce
|
|
|
|
|
dimensionality and discover latent patterns in the data.
|
2015-05-24 23:03:45 +08:00
|
|
|
|
2012-12-16 00:48:17 +08:00
|
|
|
It can be noted that k-means (and minibatch k-means) are very sensitive to
|
2012-12-15 01:53:20 +08:00
|
|
|
feature scaling and that in this case the IDF weighting helps improve the
|
|
|
|
|
quality of the clustering by quite a lot as measured against the "ground truth"
|
|
|
|
|
provided by the class label assignments of the 20 newsgroups dataset.
|
|
|
|
|
|
|
|
|
|
This improvement is not visible in the Silhouette Coefficient which is small
|
2013-04-12 02:51:28 +08:00
|
|
|
for both as this measure seem to suffer from the phenomenon called
|
2012-12-15 01:53:20 +08:00
|
|
|
"Concentration of Measure" or "Curse of Dimensionality" for high dimensional
|
|
|
|
|
datasets such as text data. Other measures such as V-measure and Adjusted Rand
|
|
|
|
|
Index are information theoretic based evaluation scores: as they are only based
|
2013-04-14 22:34:53 +08:00
|
|
|
on cluster assignments rather than distances, hence not affected by the curse
|
2012-12-15 01:53:20 +08:00
|
|
|
of dimensionality.
|
|
|
|
|
|
2013-02-01 22:04:03 +08:00
|
|
|
Note: as k-means is optimizing a non-convex objective function, it will likely
|
2012-12-15 01:53:20 +08:00
|
|
|
end up in a local optimum. Several runs with independent random init might be
|
|
|
|
|
necessary to get a good convergence.
|
2011-12-21 20:09:06 +08:00
|
|
|
|
2011-06-07 00:40:00 +08:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# Author: Peter Prettenhofer <peter.prettenhofer@gmail.com>
|
2016-03-04 17:41:12 +08:00
|
|
|
# Lars Buitinck
|
2013-04-30 14:23:46 +08:00
|
|
|
# License: BSD 3 clause
|
2021-10-22 21:33:22 +08:00
|
|
|
|
2011-09-02 17:00:02 +08:00
|
|
|
from sklearn.datasets import fetch_20newsgroups
|
2013-01-05 22:06:32 +08:00
|
|
|
from sklearn.decomposition import TruncatedSVD
|
2012-03-07 22:48:56 +08:00
|
|
|
from sklearn.feature_extraction.text import TfidfVectorizer
|
2012-12-15 01:53:20 +08:00
|
|
|
from sklearn.feature_extraction.text import HashingVectorizer
|
|
|
|
|
from sklearn.feature_extraction.text import TfidfTransformer
|
2013-12-19 06:37:03 +08:00
|
|
|
from sklearn.pipeline import make_pipeline
|
2013-05-28 17:56:32 +08:00
|
|
|
from sklearn.preprocessing import Normalizer
|
2011-09-02 17:00:02 +08:00
|
|
|
from sklearn import metrics
|
2011-06-07 00:40:00 +08:00
|
|
|
|
2011-12-21 20:09:06 +08:00
|
|
|
from sklearn.cluster import KMeans, MiniBatchKMeans
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
from optparse import OptionParser
|
|
|
|
|
import sys
|
|
|
|
|
from time import time
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
2011-06-07 00:40:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# Display progress logs on stdout
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
|
|
|
|
2011-12-21 20:09:06 +08:00
|
|
|
# parse commandline arguments
|
|
|
|
|
op = OptionParser()
|
2013-01-05 22:06:32 +08:00
|
|
|
op.add_option(
|
|
|
|
|
"--lsa",
|
|
|
|
|
dest="n_components",
|
|
|
|
|
type="int",
|
|
|
|
|
help="Preprocess documents with latent semantic analysis.",
|
|
|
|
|
)
|
2011-12-21 20:09:06 +08:00
|
|
|
op.add_option(
|
|
|
|
|
"--no-minibatch",
|
|
|
|
|
action="store_false",
|
|
|
|
|
dest="minibatch",
|
|
|
|
|
default=True,
|
2012-12-15 01:53:20 +08:00
|
|
|
help="Use ordinary k-means algorithm (in batch mode).",
|
|
|
|
|
)
|
|
|
|
|
op.add_option(
|
|
|
|
|
"--no-idf",
|
|
|
|
|
action="store_false",
|
|
|
|
|
dest="use_idf",
|
|
|
|
|
default=True,
|
|
|
|
|
help="Disable Inverse Document Frequency feature weighting.",
|
|
|
|
|
)
|
|
|
|
|
op.add_option(
|
|
|
|
|
"--use-hashing",
|
|
|
|
|
action="store_true",
|
|
|
|
|
default=False,
|
|
|
|
|
help="Use a hashing feature vectorizer",
|
|
|
|
|
)
|
|
|
|
|
op.add_option(
|
|
|
|
|
"--n-features",
|
|
|
|
|
type=int,
|
|
|
|
|
default=10000,
|
2013-01-20 21:55:15 +08:00
|
|
|
help="Maximum number of features (dimensions) to extract from text.",
|
2013-08-24 09:27:38 +08:00
|
|
|
)
|
2013-01-05 22:06:32 +08:00
|
|
|
op.add_option(
|
|
|
|
|
"--verbose",
|
|
|
|
|
action="store_true",
|
|
|
|
|
dest="verbose",
|
|
|
|
|
default=False,
|
|
|
|
|
help="Print progress reports inside k-means algorithm.",
|
|
|
|
|
)
|
2011-12-21 20:09:06 +08:00
|
|
|
|
2013-02-01 22:04:03 +08:00
|
|
|
print(__doc__)
|
2011-12-21 20:09:06 +08:00
|
|
|
op.print_help()
|
2021-10-22 21:33:22 +08:00
|
|
|
print()
|
2011-12-21 20:09:06 +08:00
|
|
|
|
2016-12-09 00:53:01 +08:00
|
|
|
|
|
|
|
|
def is_interactive():
|
|
|
|
|
return not hasattr(sys.modules["__main__"], "__file__")
|
|
|
|
|
|
2017-12-13 20:02:39 +08:00
|
|
|
|
2016-12-09 00:53:01 +08:00
|
|
|
# work-around for Jupyter notebook and IPython console
|
|
|
|
|
argv = [] if is_interactive() else sys.argv[1:]
|
|
|
|
|
(opts, args) = op.parse_args(argv)
|
2011-12-21 20:09:06 +08:00
|
|
|
if len(args) > 0:
|
|
|
|
|
op.error("this script takes no arguments.")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
2017-06-20 20:48:57 +08:00
|
|
|
# #############################################################################
|
2011-06-07 00:40:00 +08:00
|
|
|
# Load some categories from the training set
|
|
|
|
|
categories = [
|
|
|
|
|
"alt.atheism",
|
|
|
|
|
"talk.religion.misc",
|
|
|
|
|
"comp.graphics",
|
|
|
|
|
"sci.space",
|
|
|
|
|
]
|
|
|
|
|
# Uncomment the following to do the analysis on all the categories
|
2016-12-09 00:53:01 +08:00
|
|
|
# categories = None
|
2011-06-07 00:40:00 +08:00
|
|
|
|
2013-02-01 22:04:03 +08:00
|
|
|
print("Loading 20 newsgroups dataset for categories:")
|
|
|
|
|
print(categories)
|
2011-06-07 00:40:00 +08:00
|
|
|
|
2011-10-09 17:20:56 +08:00
|
|
|
dataset = fetch_20newsgroups(
|
|
|
|
|
subset="all", categories=categories, shuffle=True, random_state=42
|
|
|
|
|
)
|
2011-06-07 03:53:31 +08:00
|
|
|
|
2013-02-01 22:04:03 +08:00
|
|
|
print("%d documents" % len(dataset.data))
|
|
|
|
|
print("%d categories" % len(dataset.target_names))
|
|
|
|
|
print()
|
2011-06-07 00:40:00 +08:00
|
|
|
|
2011-10-09 17:20:56 +08:00
|
|
|
labels = dataset.target
|
2011-06-07 00:40:00 +08:00
|
|
|
true_k = np.unique(labels).shape[0]
|
|
|
|
|
|
2017-12-13 20:02:39 +08:00
|
|
|
print("Extracting features from the training dataset using a sparse vectorizer")
|
2011-06-07 00:40:00 +08:00
|
|
|
t0 = time()
|
2012-12-15 01:53:20 +08:00
|
|
|
if opts.use_hashing:
|
|
|
|
|
if opts.use_idf:
|
|
|
|
|
# Perform an IDF normalization on the output of HashingVectorizer
|
|
|
|
|
hasher = HashingVectorizer(
|
|
|
|
|
n_features=opts.n_features,
|
2017-06-21 07:01:55 +08:00
|
|
|
stop_words="english",
|
|
|
|
|
alternate_sign=False,
|
2019-08-25 10:53:18 +08:00
|
|
|
norm=None,
|
|
|
|
|
)
|
2013-12-19 06:37:03 +08:00
|
|
|
vectorizer = make_pipeline(hasher, TfidfTransformer())
|
2012-12-15 01:53:20 +08:00
|
|
|
else:
|
|
|
|
|
vectorizer = HashingVectorizer(
|
|
|
|
|
n_features=opts.n_features,
|
2013-01-20 21:55:15 +08:00
|
|
|
stop_words="english",
|
2019-08-25 10:53:18 +08:00
|
|
|
alternate_sign=False,
|
|
|
|
|
norm="l2",
|
|
|
|
|
)
|
2012-12-15 01:53:20 +08:00
|
|
|
else:
|
|
|
|
|
vectorizer = TfidfVectorizer(
|
|
|
|
|
max_df=0.5,
|
|
|
|
|
max_features=opts.n_features,
|
2013-12-28 22:39:02 +08:00
|
|
|
min_df=2,
|
|
|
|
|
stop_words="english",
|
|
|
|
|
use_idf=opts.use_idf,
|
|
|
|
|
)
|
2011-10-09 17:20:56 +08:00
|
|
|
X = vectorizer.fit_transform(dataset.data)
|
2011-06-07 00:40:00 +08:00
|
|
|
|
2013-02-01 22:04:03 +08:00
|
|
|
print("done in %fs" % (time() - t0))
|
|
|
|
|
print("n_samples: %d, n_features: %d" % X.shape)
|
|
|
|
|
print()
|
2011-06-07 00:40:00 +08:00
|
|
|
|
2013-01-05 22:06:32 +08:00
|
|
|
if opts.n_components:
|
|
|
|
|
print("Performing dimensionality reduction using LSA")
|
|
|
|
|
t0 = time()
|
2013-05-28 21:08:05 +08:00
|
|
|
# Vectorizer results are normalized, which makes KMeans behave as
|
|
|
|
|
# spherical k-means for better results. Since LSA/SVD results are
|
|
|
|
|
# not normalized, we have to redo the normalization.
|
2014-04-15 07:53:26 +08:00
|
|
|
svd = TruncatedSVD(opts.n_components)
|
2015-05-24 23:03:45 +08:00
|
|
|
normalizer = Normalizer(copy=False)
|
|
|
|
|
lsa = make_pipeline(svd, normalizer)
|
2014-04-15 07:53:26 +08:00
|
|
|
|
2013-12-19 06:37:03 +08:00
|
|
|
X = lsa.fit_transform(X)
|
2013-01-05 22:06:32 +08:00
|
|
|
|
|
|
|
|
print("done in %fs" % (time() - t0))
|
2014-04-15 07:53:26 +08:00
|
|
|
|
|
|
|
|
explained_variance = svd.explained_variance_ratio_.sum()
|
|
|
|
|
print(
|
|
|
|
|
"Explained variance of the SVD step: {}%".format(int(explained_variance * 100))
|
|
|
|
|
)
|
|
|
|
|
|
2013-01-05 22:06:32 +08:00
|
|
|
print()
|
|
|
|
|
|
2011-12-21 20:09:06 +08:00
|
|
|
|
2017-06-20 20:48:57 +08:00
|
|
|
# #############################################################################
|
2011-12-21 20:09:06 +08:00
|
|
|
# Do the actual clustering
|
|
|
|
|
|
|
|
|
|
if opts.minibatch:
|
2012-05-16 06:09:47 +08:00
|
|
|
km = MiniBatchKMeans(
|
|
|
|
|
n_clusters=true_k,
|
|
|
|
|
init="k-means++",
|
|
|
|
|
n_init=1,
|
2013-01-05 22:06:32 +08:00
|
|
|
init_size=1000,
|
|
|
|
|
batch_size=1000,
|
|
|
|
|
verbose=opts.verbose,
|
|
|
|
|
)
|
2011-12-21 20:09:06 +08:00
|
|
|
else:
|
2012-12-15 01:53:20 +08:00
|
|
|
km = KMeans(
|
|
|
|
|
n_clusters=true_k,
|
|
|
|
|
init="k-means++",
|
|
|
|
|
max_iter=100,
|
|
|
|
|
n_init=1,
|
2013-01-05 22:06:32 +08:00
|
|
|
verbose=opts.verbose,
|
|
|
|
|
)
|
2011-06-07 21:41:32 +08:00
|
|
|
|
2013-02-01 22:04:03 +08:00
|
|
|
print("Clustering sparse data with %s" % km)
|
2011-06-07 00:40:00 +08:00
|
|
|
t0 = time()
|
2011-12-21 20:09:06 +08:00
|
|
|
km.fit(X)
|
2013-02-01 22:04:03 +08:00
|
|
|
print("done in %0.3fs" % (time() - t0))
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
print("Homogeneity: %0.3f" % metrics.homogeneity_score(labels, km.labels_))
|
|
|
|
|
print("Completeness: %0.3f" % metrics.completeness_score(labels, km.labels_))
|
|
|
|
|
print("V-measure: %0.3f" % metrics.v_measure_score(labels, km.labels_))
|
|
|
|
|
print("Adjusted Rand-Index: %.3f" % metrics.adjusted_rand_score(labels, km.labels_))
|
|
|
|
|
print(
|
|
|
|
|
"Silhouette Coefficient: %0.3f"
|
2014-11-06 03:45:00 +08:00
|
|
|
% metrics.silhouette_score(X, km.labels_, sample_size=1000)
|
2021-10-07 16:13:00 +08:00
|
|
|
)
|
2013-02-01 22:04:03 +08:00
|
|
|
|
|
|
|
|
print()
|
2013-12-28 22:39:02 +08:00
|
|
|
|
2015-05-24 23:03:45 +08:00
|
|
|
|
|
|
|
|
if not opts.use_hashing:
|
2013-12-28 22:39:02 +08:00
|
|
|
print("Top terms per cluster:")
|
2015-05-24 23:03:45 +08:00
|
|
|
|
|
|
|
|
if opts.n_components:
|
|
|
|
|
original_space_centroids = svd.inverse_transform(km.cluster_centers_)
|
|
|
|
|
order_centroids = original_space_centroids.argsort()[:, ::-1]
|
|
|
|
|
else:
|
|
|
|
|
order_centroids = km.cluster_centers_.argsort()[:, ::-1]
|
|
|
|
|
|
2021-09-07 16:56:57 +08:00
|
|
|
terms = vectorizer.get_feature_names_out()
|
2014-04-15 07:53:26 +08:00
|
|
|
for i in range(true_k):
|
2013-12-28 22:39:02 +08:00
|
|
|
print("Cluster %d:" % i, end="")
|
|
|
|
|
for ind in order_centroids[i, :10]:
|
|
|
|
|
print(" %s" % terms[ind], end="")
|
|
|
|
|
print()
|