/README.rst

http://github.com/apresta/tagger · ReStructuredText · 117 lines · 89 code · 28 blank · 0 comment · 0 complexity · 51b1b5650377dc2fc41746b6fcafafec MD5 · raw file

  1. ======
  2. tagger
  3. ======
  4. Module for extracting tags from text documents.
  5. Copyright (C) 2011 by Alessandro Presta
  6. Configuration
  7. =============
  8. Dependencies:
  9. python2.7, stemming, nltk (optional), lxml (optional)
  10. You can install the stemming package with::
  11. $ easy_install stemming
  12. Usage
  13. =====
  14. Tagging a text document from Python::
  15. import tagger
  16. weights = pickle.load(open('data/dict.pkl', 'rb')) # or your own dictionary
  17. myreader = tagger.Reader() # or your own reader class
  18. mystemmer = tagger.Stemmer() # or your own stemmer class
  19. myrater = tagger.Rater(weights) # or your own... (you got the idea)
  20. mytagger = Tagger(myreader, mystemmer, myrater)
  21. best_3_tags = mytagger(text_string, 3)
  22. Running the module as a script::
  23. $ ./tagger.py <text document(s) to tag>
  24. Example::
  25. $ ./tagger.py tests/*
  26. Loading dictionary...
  27. Tags for tests/bbc1.txt :
  28. ['bin laden', 'obama', 'pakistan', 'killed', 'raid']
  29. Tags for tests/bbc2.txt :
  30. ['jo yeates', 'bristol', 'vincent tabak', 'murder', 'strangled']
  31. Tags for tests/bbc3.txt :
  32. ['snp', 'party', 'election', 'scottish', 'labour']
  33. Tags for tests/guardian1.txt :
  34. ['bin laden', 'al-qaida', 'killed', 'pakistan', 'al-fawwaz']
  35. Tags for tests/guardian2.txt :
  36. ['clegg', 'tory', 'lib dem', 'party', 'coalition']
  37. Tags for tests/post1.txt :
  38. ['sony', 'stolen', 'playstation network', 'hacker attack', 'lawsuit']
  39. Tags for tests/wikipedia1.txt :
  40. ['universe', 'anthropic principle', 'observed', 'cosmological', 'theory']
  41. Tags for tests/wikipedia2.txt :
  42. ['beetroot', 'beet', 'betaine', 'blood pressure', 'dietary nitrate']
  43. Tags for tests/wikipedia3.txt :
  44. ['the lounge lizards', 'jazz', 'john lurie', 'musical', 'albums']
  45. A brief explanation
  46. ===================
  47. Extracting tags from a text document involves at least three steps: splitting the document into words, grouping together variants of the same word, and ranking them according to their relevance.
  48. These three tasks are carried out respectively by the **Reader**, **Stemmer** and **Rater** classes, and their work is put together by the **Tagger** class.
  49. A **Reader** object may accept as input a document in some format, perform some normalisation of the text (such as turning everything into lower case), analyse the structure of the phrases and punctuation, and return a list of words respecting the order in the text, perhaps with some additional information such as which ones look like proper nouns, or are at the end of a phrase.
  50. A very straightforward way of doing this would be to just match all the words with a regular expression, and this is indeed what the **SimpleReader** class does.
  51. The **Stemmer** tries to recognise the root of a word, in order to identify slightly different forms. This is already a quite complicated task, and it's clearly language-specific.
  52. The *stem* module in the NLTK package provides algorithms for many languages
  53. and integrates nicely with the tagger::
  54. import nltk
  55. # an English stemmer using Lancaster's algorithm
  56. mystemmer = Stemmer(nltk.stem.LancasterStemmer)
  57. # an Italian stemmer
  58. class MyItalianStemmer(Stemmer):
  59. def __init__(self):
  60. Stemmer.__init__(self, nltk.stem.ItalianStemmer)
  61. def preprocess(self, string):
  62. # do something with the string before passing it to nltk's stemmer
  63. The **Rater** takes the list of words contained in the document, together with any additional information gathered at the previous stages, and returns a list of tags (i.e. words or small units of text) ordered by some idea of "relevance".
  64. It turns out that just working on the information contained in the document itself is not enough, because it says nothing about the frequency of a term in the language. For this reason, an early "off-line" phase of the algorithm consists in analysing a *corpus* (i.e. a sample of documents written in the same language) to build a dictionary of known words. This is taken care by the **build_dict()** function.
  65. It is advised to build your own dictionaries, and the **build_dict_from_nltk()** function in the *extras* module enables you to use the corpora included in NLTK::
  66. build_dict_from_nltk(output_file, nltk.corpus.brown,
  67. nltk.corpus.stopwords.words('english'), measure='ICF')
  68. So far, we may define the relevance of a word as the product of two distinct functions: one that depends on the document itself, and one that depends on the corpus.
  69. A standard measure in information retrieval is TF-IDF (*term frequency-inverse
  70. document frequency*): the frequency of the word in the document multiplied by
  71. the (logarithm of) the inverse of its frequency in the corpus (i.e. the cardinality of the corpus divided by the number of documents where the word is found).
  72. If we treat the whole corpus as a single document, and count the total occurrences of the term instead, we obtain ICF (*inverse collection frequency*).
  73. Both of these are implemented in the *build_dict* module, and any other reasonable measure should be fine, provided that it is normalised in the interval [0,1]. The dictionary is passed to the **Rater** object as the *weights* argument in its constructor.
  74. We might also want to define the first term of the product in a different way, and this is done by overriding the **rate_tags()** method (which by default calculates TF for each word and multiplies it by its weight)::
  75. class MyRater(Rater):
  76. def rate_tags(self, tags):
  77. # set each tag's rating as you wish
  78. If we were not too picky about the results, these few bits would already make an acceptable tagger.
  79. However, it's a matter of fact that tags formed only by single words are quite limited: while "obama" and "barack obama" are both reasonable tags (and it is quite easy to treat cases like this in order to regard them as equal), having "laden" and "bin" as two separate tags is definitely not acceptable and misleading.
  80. Compare the results on the same document using the **NaiveRater** class (defined in the module *extras*) instead of the standard one.
  81. The *multitag_size* parameter in the **Rater**'s constructor defines the maximum number of words that can constitute a tag. Multitags are generated in the **create_multitags()** method; if additional information about the position of a word in the phrase is available (i.e. the **terminal** member of the class **Tag**), this can be done in a more accurate way.
  82. The rating of a **MultiTag** is computed from the ratings of its unit tags.
  83. By default, the **combined_rating()** method uses the geometric mean, with a special treatment of proper nouns if that information is available too (in the **proper** member).
  84. This method can be overridden too, so there is room for experimentation.
  85. With a few "common sense" heuristics the results are greatly improved.
  86. The final stage of the default rating algorithm involves discarding redundant tags (i.e. tags that contain or are contained in other, less relevant tags).
  87. It should be stressed that the default implementation doesn't make any assumption on the type of document that is being tagged (except for it being written in English) and on the kinds of tags that should be given priority (which sometimes can be a matter of taste or depend on the particular task we are using the tags for).
  88. With some additional assumptions and an accurate treatment of corner cases, the tagger can be tailored to suit the user's needs.
  89. This is proof-of-concept software and extensive experimentation is encouraged. The design of the base classes should allow for this, and the few examples in the *extras* module are a good starting point for customising the algorithm.