util.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. # Natural Language Toolkit: Clusterer Utilities
  2. #
  3. # Copyright (C) 2001-2020 NLTK Project
  4. # Author: Trevor Cohn <tacohn@cs.mu.oz.au>
  5. # Contributor: J Richard Snape
  6. # URL: <http://nltk.org/>
  7. # For license information, see LICENSE.TXT
  8. from abc import abstractmethod
  9. import copy
  10. from sys import stdout
  11. from math import sqrt
  12. try:
  13. import numpy
  14. except ImportError:
  15. pass
  16. from nltk.cluster.api import ClusterI
  17. class VectorSpaceClusterer(ClusterI):
  18. """
  19. Abstract clusterer which takes tokens and maps them into a vector space.
  20. Optionally performs singular value decomposition to reduce the
  21. dimensionality.
  22. """
  23. def __init__(self, normalise=False, svd_dimensions=None):
  24. """
  25. :param normalise: should vectors be normalised to length 1
  26. :type normalise: boolean
  27. :param svd_dimensions: number of dimensions to use in reducing vector
  28. dimensionsionality with SVD
  29. :type svd_dimensions: int
  30. """
  31. self._Tt = None
  32. self._should_normalise = normalise
  33. self._svd_dimensions = svd_dimensions
  34. def cluster(self, vectors, assign_clusters=False, trace=False):
  35. assert len(vectors) > 0
  36. # normalise the vectors
  37. if self._should_normalise:
  38. vectors = list(map(self._normalise, vectors))
  39. # use SVD to reduce the dimensionality
  40. if self._svd_dimensions and self._svd_dimensions < len(vectors[0]):
  41. [u, d, vt] = numpy.linalg.svd(numpy.transpose(numpy.array(vectors)))
  42. S = d[: self._svd_dimensions] * numpy.identity(
  43. self._svd_dimensions, numpy.float64
  44. )
  45. T = u[:, : self._svd_dimensions]
  46. Dt = vt[: self._svd_dimensions, :]
  47. vectors = numpy.transpose(numpy.dot(S, Dt))
  48. self._Tt = numpy.transpose(T)
  49. # call abstract method to cluster the vectors
  50. self.cluster_vectorspace(vectors, trace)
  51. # assign the vectors to clusters
  52. if assign_clusters:
  53. return [self.classify(vector) for vector in vectors]
  54. @abstractmethod
  55. def cluster_vectorspace(self, vectors, trace):
  56. """
  57. Finds the clusters using the given set of vectors.
  58. """
  59. def classify(self, vector):
  60. if self._should_normalise:
  61. vector = self._normalise(vector)
  62. if self._Tt is not None:
  63. vector = numpy.dot(self._Tt, vector)
  64. cluster = self.classify_vectorspace(vector)
  65. return self.cluster_name(cluster)
  66. @abstractmethod
  67. def classify_vectorspace(self, vector):
  68. """
  69. Returns the index of the appropriate cluster for the vector.
  70. """
  71. def likelihood(self, vector, label):
  72. if self._should_normalise:
  73. vector = self._normalise(vector)
  74. if self._Tt is not None:
  75. vector = numpy.dot(self._Tt, vector)
  76. return self.likelihood_vectorspace(vector, label)
  77. def likelihood_vectorspace(self, vector, cluster):
  78. """
  79. Returns the likelihood of the vector belonging to the cluster.
  80. """
  81. predicted = self.classify_vectorspace(vector)
  82. return 1.0 if cluster == predicted else 0.0
  83. def vector(self, vector):
  84. """
  85. Returns the vector after normalisation and dimensionality reduction
  86. """
  87. if self._should_normalise:
  88. vector = self._normalise(vector)
  89. if self._Tt is not None:
  90. vector = numpy.dot(self._Tt, vector)
  91. return vector
  92. def _normalise(self, vector):
  93. """
  94. Normalises the vector to unit length.
  95. """
  96. return vector / sqrt(numpy.dot(vector, vector))
  97. def euclidean_distance(u, v):
  98. """
  99. Returns the euclidean distance between vectors u and v. This is equivalent
  100. to the length of the vector (u - v).
  101. """
  102. diff = u - v
  103. return sqrt(numpy.dot(diff, diff))
  104. def cosine_distance(u, v):
  105. """
  106. Returns 1 minus the cosine of the angle between vectors v and u. This is
  107. equal to 1 - (u.v / |u||v|).
  108. """
  109. return 1 - (numpy.dot(u, v) / (sqrt(numpy.dot(u, u)) * sqrt(numpy.dot(v, v))))
  110. class _DendrogramNode(object):
  111. """ Tree node of a dendrogram. """
  112. def __init__(self, value, *children):
  113. self._value = value
  114. self._children = children
  115. def leaves(self, values=True):
  116. if self._children:
  117. leaves = []
  118. for child in self._children:
  119. leaves.extend(child.leaves(values))
  120. return leaves
  121. elif values:
  122. return [self._value]
  123. else:
  124. return [self]
  125. def groups(self, n):
  126. queue = [(self._value, self)]
  127. while len(queue) < n:
  128. priority, node = queue.pop()
  129. if not node._children:
  130. queue.push((priority, node))
  131. break
  132. for child in node._children:
  133. if child._children:
  134. queue.append((child._value, child))
  135. else:
  136. queue.append((0, child))
  137. # makes the earliest merges at the start, latest at the end
  138. queue.sort()
  139. groups = []
  140. for priority, node in queue:
  141. groups.append(node.leaves())
  142. return groups
  143. def __lt__(self, comparator):
  144. return cosine_distance(self._value, comparator._value) < 0
  145. class Dendrogram(object):
  146. """
  147. Represents a dendrogram, a tree with a specified branching order. This
  148. must be initialised with the leaf items, then iteratively call merge for
  149. each branch. This class constructs a tree representing the order of calls
  150. to the merge function.
  151. """
  152. def __init__(self, items=[]):
  153. """
  154. :param items: the items at the leaves of the dendrogram
  155. :type items: sequence of (any)
  156. """
  157. self._items = [_DendrogramNode(item) for item in items]
  158. self._original_items = copy.copy(self._items)
  159. self._merge = 1
  160. def merge(self, *indices):
  161. """
  162. Merges nodes at given indices in the dendrogram. The nodes will be
  163. combined which then replaces the first node specified. All other nodes
  164. involved in the merge will be removed.
  165. :param indices: indices of the items to merge (at least two)
  166. :type indices: seq of int
  167. """
  168. assert len(indices) >= 2
  169. node = _DendrogramNode(self._merge, *[self._items[i] for i in indices])
  170. self._merge += 1
  171. self._items[indices[0]] = node
  172. for i in indices[1:]:
  173. del self._items[i]
  174. def groups(self, n):
  175. """
  176. Finds the n-groups of items (leaves) reachable from a cut at depth n.
  177. :param n: number of groups
  178. :type n: int
  179. """
  180. if len(self._items) > 1:
  181. root = _DendrogramNode(self._merge, *self._items)
  182. else:
  183. root = self._items[0]
  184. return root.groups(n)
  185. def show(self, leaf_labels=[]):
  186. """
  187. Print the dendrogram in ASCII art to standard out.
  188. :param leaf_labels: an optional list of strings to use for labeling the
  189. leaves
  190. :type leaf_labels: list
  191. """
  192. # ASCII rendering characters
  193. JOIN, HLINK, VLINK = "+", "-", "|"
  194. # find the root (or create one)
  195. if len(self._items) > 1:
  196. root = _DendrogramNode(self._merge, *self._items)
  197. else:
  198. root = self._items[0]
  199. leaves = self._original_items
  200. if leaf_labels:
  201. last_row = leaf_labels
  202. else:
  203. last_row = ["%s" % leaf._value for leaf in leaves]
  204. # find the bottom row and the best cell width
  205. width = max(map(len, last_row)) + 1
  206. lhalf = width // 2
  207. rhalf = int(width - lhalf - 1)
  208. # display functions
  209. def format(centre, left=" ", right=" "):
  210. return "%s%s%s" % (lhalf * left, centre, right * rhalf)
  211. def display(str):
  212. stdout.write(str)
  213. # for each merge, top down
  214. queue = [(root._value, root)]
  215. verticals = [format(" ") for leaf in leaves]
  216. while queue:
  217. priority, node = queue.pop()
  218. child_left_leaf = list(map(lambda c: c.leaves(False)[0], node._children))
  219. indices = list(map(leaves.index, child_left_leaf))
  220. if child_left_leaf:
  221. min_idx = min(indices)
  222. max_idx = max(indices)
  223. for i in range(len(leaves)):
  224. if leaves[i] in child_left_leaf:
  225. if i == min_idx:
  226. display(format(JOIN, " ", HLINK))
  227. elif i == max_idx:
  228. display(format(JOIN, HLINK, " "))
  229. else:
  230. display(format(JOIN, HLINK, HLINK))
  231. verticals[i] = format(VLINK)
  232. elif min_idx <= i <= max_idx:
  233. display(format(HLINK, HLINK, HLINK))
  234. else:
  235. display(verticals[i])
  236. display("\n")
  237. for child in node._children:
  238. if child._children:
  239. queue.append((child._value, child))
  240. queue.sort()
  241. for vertical in verticals:
  242. display(vertical)
  243. display("\n")
  244. # finally, display the last line
  245. display("".join(item.center(width) for item in last_row))
  246. display("\n")
  247. def __repr__(self):
  248. if len(self._items) > 1:
  249. root = _DendrogramNode(self._merge, *self._items)
  250. else:
  251. root = self._items[0]
  252. leaves = root.leaves(False)
  253. return "<Dendrogram with %d leaves>" % len(leaves)