nltk 3.2__zip → 3.3__zip

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (406) hide show
  1. {nltk-3.2 → nltk-3.3}/INSTALL.txt +6 -6
  2. {nltk-3.2 → nltk-3.3}/LICENSE.txt +13 -13
  3. {nltk-3.2 → nltk-3.3}/MANIFEST.in +6 -6
  4. {nltk-3.2/nltk.egg-info → nltk-3.3}/PKG-INFO +13 -5
  5. nltk-3.2/README.txt → nltk-3.3/README.md +49 -48
  6. nltk-3.3/nltk/VERSION +1 -0
  7. {nltk-3.2 → nltk-3.3}/nltk/__init__.py +185 -185
  8. {nltk-3.2 → nltk-3.3}/nltk/app/__init__.py +52 -53
  9. {nltk-3.2 → nltk-3.3}/nltk/app/chartparser_app.py +2276 -2276
  10. {nltk-3.2 → nltk-3.3}/nltk/app/chunkparser_app.py +1262 -1264
  11. {nltk-3.2 → nltk-3.3}/nltk/app/collocations_app.py +347 -350
  12. {nltk-3.2 → nltk-3.3}/nltk/app/concordance_app.py +568 -570
  13. {nltk-3.2 → nltk-3.3}/nltk/app/nemo_app.py +156 -155
  14. {nltk-3.2 → nltk-3.3}/nltk/app/rdparser_app.py +892 -894
  15. {nltk-3.2 → nltk-3.3}/nltk/app/srparser_app.py +809 -809
  16. {nltk-3.2 → nltk-3.3}/nltk/app/wordfreq_app.py +32 -32
  17. {nltk-3.2 → nltk-3.3}/nltk/app/wordnet_app.py +968 -968
  18. {nltk-3.2 → nltk-3.3}/nltk/book.py +91 -90
  19. {nltk-3.2 → nltk-3.3}/nltk/ccg/__init__.py +22 -22
  20. {nltk-3.2 → nltk-3.3}/nltk/ccg/api.py +359 -335
  21. {nltk-3.2 → nltk-3.3}/nltk/ccg/chart.py +394 -361
  22. {nltk-3.2 → nltk-3.3}/nltk/ccg/combinator.py +343 -312
  23. {nltk-3.2 → nltk-3.3}/nltk/ccg/lexicon.py +328 -290
  24. nltk-3.3/nltk/ccg/logic.py +46 -0
  25. {nltk-3.2 → nltk-3.3}/nltk/chat/__init__.py +49 -49
  26. {nltk-3.2 → nltk-3.3}/nltk/chat/eliza.py +244 -244
  27. {nltk-3.2 → nltk-3.3}/nltk/chat/iesha.py +140 -140
  28. {nltk-3.2 → nltk-3.3}/nltk/chat/rude.py +92 -92
  29. {nltk-3.2 → nltk-3.3}/nltk/chat/suntsu.py +117 -117
  30. {nltk-3.2 → nltk-3.3}/nltk/chat/util.py +122 -120
  31. {nltk-3.2 → nltk-3.3}/nltk/chat/zen.py +282 -282
  32. {nltk-3.2 → nltk-3.3}/nltk/chunk/__init__.py +190 -190
  33. {nltk-3.2 → nltk-3.3}/nltk/chunk/api.py +51 -51
  34. {nltk-3.2 → nltk-3.3}/nltk/chunk/named_entity.py +331 -331
  35. {nltk-3.2 → nltk-3.3}/nltk/chunk/regexp.py +1390 -1384
  36. {nltk-3.2 → nltk-3.3}/nltk/chunk/util.py +600 -600
  37. {nltk-3.2 → nltk-3.3}/nltk/classify/__init__.py +98 -98
  38. {nltk-3.2 → nltk-3.3}/nltk/classify/api.py +193 -193
  39. {nltk-3.2 → nltk-3.3}/nltk/classify/decisiontree.py +295 -295
  40. {nltk-3.2 → nltk-3.3}/nltk/classify/maxent.py +1510 -1492
  41. {nltk-3.2 → nltk-3.3}/nltk/classify/megam.py +179 -178
  42. {nltk-3.2 → nltk-3.3}/nltk/classify/naivebayes.py +245 -242
  43. {nltk-3.2 → nltk-3.3}/nltk/classify/positivenaivebayes.py +170 -170
  44. {nltk-3.2 → nltk-3.3}/nltk/classify/rte_classify.py +160 -191
  45. {nltk-3.2 → nltk-3.3}/nltk/classify/scikitlearn.py +153 -151
  46. {nltk-3.2 → nltk-3.3}/nltk/classify/senna.py +183 -181
  47. {nltk-3.2 → nltk-3.3}/nltk/classify/svm.py +15 -15
  48. {nltk-3.2 → nltk-3.3}/nltk/classify/tadm.py +113 -112
  49. {nltk-3.2 → nltk-3.3}/nltk/classify/textcat.py +193 -193
  50. {nltk-3.2 → nltk-3.3}/nltk/classify/util.py +324 -312
  51. {nltk-3.2 → nltk-3.3}/nltk/classify/weka.py +346 -345
  52. {nltk-3.2 → nltk-3.3}/nltk/cluster/__init__.py +86 -86
  53. {nltk-3.2 → nltk-3.3}/nltk/cluster/api.py +74 -70
  54. {nltk-3.2 → nltk-3.3}/nltk/cluster/em.py +247 -247
  55. {nltk-3.2 → nltk-3.3}/nltk/cluster/gaac.py +168 -168
  56. {nltk-3.2 → nltk-3.3}/nltk/cluster/kmeans.py +221 -221
  57. {nltk-3.2 → nltk-3.3}/nltk/cluster/util.py +304 -289
  58. nltk-3.3/nltk/collections.py +640 -0
  59. {nltk-3.2 → nltk-3.3}/nltk/collocations.py +388 -388
  60. nltk-3.3/nltk/compat.py +365 -0
  61. {nltk-3.2 → nltk-3.3}/nltk/corpus/__init__.py +341 -320
  62. {nltk-3.2 → nltk-3.3}/nltk/corpus/europarl_raw.py +44 -44
  63. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/__init__.py +147 -145
  64. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/aligned.py +115 -114
  65. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/api.py +448 -446
  66. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/bnc.py +252 -252
  67. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/bracket_parse.py +227 -227
  68. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/categorized_sents.py +179 -178
  69. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/chasen.py +140 -139
  70. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/childes.py +522 -494
  71. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/chunked.py +212 -212
  72. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/cmudict.py +97 -95
  73. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/comparative_sents.py +280 -278
  74. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/conll.py +528 -521
  75. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/crubadan.py +116 -116
  76. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/dependency.py +101 -101
  77. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/framenet.py +3059 -2093
  78. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/ieer.py +112 -111
  79. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/indian.py +85 -86
  80. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/ipipan.py +332 -331
  81. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/knbc.py +187 -187
  82. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/lin.py +156 -156
  83. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/mte.py +311 -310
  84. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/nkjp.py +429 -428
  85. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/nombank.py +421 -421
  86. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/nps_chat.py +73 -73
  87. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/opinion_lexicon.py +115 -115
  88. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/panlex_lite.py +165 -165
  89. nltk-3.3/nltk/corpus/reader/pl196x.py +292 -0
  90. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/plaintext.py +232 -233
  91. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/ppattach.py +96 -95
  92. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/propbank.py +479 -481
  93. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/pros_cons.py +128 -126
  94. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/reviews.py +330 -328
  95. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/rte.py +144 -146
  96. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/semcor.py +256 -256
  97. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/senseval.py +203 -201
  98. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/sentiwordnet.py +137 -137
  99. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/sinica_treebank.py +75 -75
  100. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/string_category.py +60 -59
  101. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/switchboard.py +119 -119
  102. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/tagged.py +295 -294
  103. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/timit.py +452 -450
  104. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/toolbox.py +68 -68
  105. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/twitter.py +155 -157
  106. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/udhr.py +80 -80
  107. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/util.py +804 -800
  108. nltk-3.3/nltk/corpus/reader/verbnet.py +595 -0
  109. nltk-3.3/nltk/corpus/reader/wordlist.py +134 -0
  110. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/wordnet.py +2108 -2036
  111. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/xmldocs.py +392 -387
  112. {nltk-3.2 → nltk-3.3}/nltk/corpus/reader/ycoe.py +243 -242
  113. {nltk-3.2 → nltk-3.3}/nltk/corpus/util.py +144 -127
  114. {nltk-3.2 → nltk-3.3}/nltk/data.py +1487 -1451
  115. {nltk-3.2 → nltk-3.3}/nltk/decorators.py +219 -215
  116. {nltk-3.2 → nltk-3.3}/nltk/downloader.py +2278 -2274
  117. {nltk-3.2 → nltk-3.3}/nltk/draw/__init__.py +27 -27
  118. {nltk-3.2 → nltk-3.3}/nltk/draw/cfg.py +774 -777
  119. {nltk-3.2 → nltk-3.3}/nltk/draw/dispersion.py +58 -58
  120. {nltk-3.2 → nltk-3.3}/nltk/draw/table.py +1098 -1098
  121. {nltk-3.2 → nltk-3.3}/nltk/draw/tree.py +963 -966
  122. {nltk-3.2 → nltk-3.3}/nltk/draw/util.py +2356 -2356
  123. {nltk-3.2 → nltk-3.3}/nltk/featstruct.py +2617 -2500
  124. {nltk-3.2 → nltk-3.3}/nltk/grammar.py +1559 -1516
  125. {nltk-3.2 → nltk-3.3}/nltk/help.py +56 -56
  126. {nltk-3.2 → nltk-3.3}/nltk/inference/__init__.py +20 -20
  127. {nltk-3.2 → nltk-3.3}/nltk/inference/api.py +597 -589
  128. {nltk-3.2 → nltk-3.3}/nltk/inference/discourse.py +612 -608
  129. {nltk-3.2 → nltk-3.3}/nltk/inference/mace.py +311 -311
  130. {nltk-3.2 → nltk-3.3}/nltk/inference/nonmonotonic.py +509 -509
  131. {nltk-3.2 → nltk-3.3}/nltk/inference/prover9.py +431 -431
  132. {nltk-3.2 → nltk-3.3}/nltk/inference/resolution.py +687 -687
  133. {nltk-3.2 → nltk-3.3}/nltk/inference/tableau.py +607 -607
  134. {nltk-3.2 → nltk-3.3}/nltk/internals.py +982 -982
  135. {nltk-3.2 → nltk-3.3}/nltk/jsontags.py +63 -63
  136. {nltk-3.2 → nltk-3.3}/nltk/lazyimport.py +142 -142
  137. {nltk-3.2 → nltk-3.3}/nltk/metrics/__init__.py +30 -29
  138. {nltk-3.2 → nltk-3.3}/nltk/metrics/agreement.py +421 -401
  139. nltk-3.3/nltk/metrics/aline.py +607 -0
  140. {nltk-3.2 → nltk-3.3}/nltk/metrics/association.py +414 -410
  141. {nltk-3.2 → nltk-3.3}/nltk/metrics/confusionmatrix.py +206 -206
  142. {nltk-3.2 → nltk-3.3}/nltk/metrics/distance.py +207 -202
  143. {nltk-3.2 → nltk-3.3}/nltk/metrics/paice.py +381 -381
  144. {nltk-3.2 → nltk-3.3}/nltk/metrics/scores.py +228 -228
  145. {nltk-3.2 → nltk-3.3}/nltk/metrics/segmentation.py +231 -232
  146. {nltk-3.2 → nltk-3.3}/nltk/metrics/spearman.py +69 -69
  147. {nltk-3.2 → nltk-3.3}/nltk/misc/__init__.py +11 -11
  148. {nltk-3.2 → nltk-3.3}/nltk/misc/babelfish.py +10 -10
  149. {nltk-3.2 → nltk-3.3}/nltk/misc/chomsky.py +135 -133
  150. {nltk-3.2 → nltk-3.3}/nltk/misc/minimalset.py +83 -83
  151. {nltk-3.2 → nltk-3.3}/nltk/misc/sort.py +157 -157
  152. {nltk-3.2 → nltk-3.3}/nltk/misc/wordfinder.py +129 -129
  153. {nltk-3.2 → nltk-3.3}/nltk/parse/__init__.py +81 -80
  154. {nltk-3.2 → nltk-3.3}/nltk/parse/api.py +66 -66
  155. {nltk-3.2 → nltk-3.3}/nltk/parse/bllip.py +282 -282
  156. {nltk-3.2 → nltk-3.3}/nltk/parse/chart.py +1682 -1681
  157. nltk-3.3/nltk/parse/corenlp.py +780 -0
  158. {nltk-3.2 → nltk-3.3}/nltk/parse/dependencygraph.py +760 -758
  159. {nltk-3.2 → nltk-3.3}/nltk/parse/earleychart.py +452 -451
  160. {nltk-3.2 → nltk-3.3}/nltk/parse/evaluate.py +129 -129
  161. {nltk-3.2 → nltk-3.3}/nltk/parse/featurechart.py +580 -578
  162. {nltk-3.2 → nltk-3.3}/nltk/parse/generate.py +87 -76
  163. {nltk-3.2 → nltk-3.3}/nltk/parse/malt.py +356 -356
  164. {nltk-3.2 → nltk-3.3}/nltk/parse/nonprojectivedependencyparser.py +783 -783
  165. {nltk-3.2 → nltk-3.3}/nltk/parse/pchart.py +522 -522
  166. {nltk-3.2 → nltk-3.3}/nltk/parse/projectivedependencyparser.py +579 -578
  167. {nltk-3.2 → nltk-3.3}/nltk/parse/recursivedescent.py +655 -656
  168. {nltk-3.2 → nltk-3.3}/nltk/parse/shiftreduce.py +458 -459
  169. {nltk-3.2 → nltk-3.3}/nltk/parse/stanford.py +431 -406
  170. {nltk-3.2 → nltk-3.3}/nltk/parse/transitionparser.py +774 -772
  171. {nltk-3.2 → nltk-3.3}/nltk/parse/util.py +231 -231
  172. {nltk-3.2 → nltk-3.3}/nltk/parse/viterbi.py +401 -401
  173. {nltk-3.2 → nltk-3.3}/nltk/probability.py +2410 -2355
  174. {nltk-3.2 → nltk-3.3}/nltk/sem/__init__.py +61 -61
  175. {nltk-3.2 → nltk-3.3}/nltk/sem/boxer.py +1261 -1261
  176. {nltk-3.2 → nltk-3.3}/nltk/sem/chat80.py +782 -782
  177. {nltk-3.2 → nltk-3.3}/nltk/sem/cooper_storage.py +118 -118
  178. {nltk-3.2 → nltk-3.3}/nltk/sem/drt.py +1258 -1257
  179. {nltk-3.2 → nltk-3.3}/nltk/sem/drt_glue_demo.py +485 -489
  180. {nltk-3.2 → nltk-3.3}/nltk/sem/evaluate.py +790 -784
  181. {nltk-3.2 → nltk-3.3}/nltk/sem/glue.py +696 -694
  182. {nltk-3.2 → nltk-3.3}/nltk/sem/hole.py +386 -384
  183. {nltk-3.2 → nltk-3.3}/nltk/sem/lfg.py +210 -208
  184. {nltk-3.2 → nltk-3.3}/nltk/sem/linearlogic.py +451 -449
  185. {nltk-3.2 → nltk-3.3}/nltk/sem/logic.py +1910 -1910
  186. {nltk-3.2 → nltk-3.3}/nltk/sem/relextract.py +476 -483
  187. {nltk-3.2 → nltk-3.3}/nltk/sem/skolemize.py +101 -101
  188. {nltk-3.2 → nltk-3.3}/nltk/sem/util.py +249 -249
  189. {nltk-3.2 → nltk-3.3}/nltk/sentiment/__init__.py +14 -14
  190. {nltk-3.2 → nltk-3.3}/nltk/sentiment/sentiment_analyzer.py +229 -229
  191. {nltk-3.2 → nltk-3.3}/nltk/sentiment/util.py +762 -752
  192. {nltk-3.2 → nltk-3.3}/nltk/sentiment/vader.py +443 -445
  193. {nltk-3.2 → nltk-3.3}/nltk/stem/__init__.py +31 -33
  194. {nltk-3.2 → nltk-3.3}/nltk/stem/api.py +28 -25
  195. nltk-3.3/nltk/stem/arlstem.py +369 -0
  196. {nltk-3.2 → nltk-3.3}/nltk/stem/isri.py +345 -345
  197. {nltk-3.2 → nltk-3.3}/nltk/stem/lancaster.py +337 -310
  198. nltk-3.3/nltk/stem/porter.py +710 -0
  199. {nltk-3.2 → nltk-3.3}/nltk/stem/regexp.py +61 -61
  200. {nltk-3.2 → nltk-3.3}/nltk/stem/rslp.py +142 -142
  201. {nltk-3.2 → nltk-3.3}/nltk/stem/snowball.py +4236 -3712
  202. {nltk-3.2 → nltk-3.3}/nltk/stem/util.py +22 -12
  203. {nltk-3.2 → nltk-3.3}/nltk/stem/wordnet.py +51 -51
  204. {nltk-3.2 → nltk-3.3}/nltk/tag/__init__.py +152 -127
  205. {nltk-3.2 → nltk-3.3}/nltk/tag/api.py +86 -81
  206. {nltk-3.2 → nltk-3.3}/nltk/tag/brill.py +424 -426
  207. {nltk-3.2 → nltk-3.3}/nltk/tag/brill_trainer.py +608 -608
  208. {nltk-3.2 → nltk-3.3}/nltk/tag/crf.py +204 -200
  209. {nltk-3.2 → nltk-3.3}/nltk/tag/hmm.py +1277 -1277
  210. {nltk-3.2 → nltk-3.3}/nltk/tag/hunpos.py +132 -132
  211. {nltk-3.2 → nltk-3.3}/nltk/tag/mapping.py +101 -101
  212. {nltk-3.2 → nltk-3.3}/nltk/tag/perceptron.py +329 -314
  213. {nltk-3.2 → nltk-3.3}/nltk/tag/senna.py +147 -145
  214. {nltk-3.2 → nltk-3.3}/nltk/tag/sequential.py +740 -736
  215. {nltk-3.2 → nltk-3.3}/nltk/tag/stanford.py +219 -196
  216. {nltk-3.2 → nltk-3.3}/nltk/tag/tnt.py +595 -604
  217. {nltk-3.2 → nltk-3.3}/nltk/tag/util.py +72 -72
  218. {nltk-3.2 → nltk-3.3}/nltk/tbl/__init__.py +28 -28
  219. {nltk-3.2 → nltk-3.3}/nltk/tbl/api.py +1 -1
  220. {nltk-3.2 → nltk-3.3}/nltk/tbl/demo.py +366 -366
  221. {nltk-3.2 → nltk-3.3}/nltk/tbl/erroranalysis.py +39 -39
  222. {nltk-3.2 → nltk-3.3}/nltk/tbl/feature.py +259 -261
  223. {nltk-3.2 → nltk-3.3}/nltk/tbl/rule.py +321 -321
  224. {nltk-3.2 → nltk-3.3}/nltk/tbl/template.py +303 -308
  225. {nltk-3.2 → nltk-3.3}/nltk/test/__init__.py +18 -18
  226. {nltk-3.2 → nltk-3.3}/nltk/test/all.py +24 -23
  227. {nltk-3.2 → nltk-3.3}/nltk/test/bleu.doctest +14 -14
  228. {nltk-3.2 → nltk-3.3}/nltk/test/bnc.doctest +60 -60
  229. {nltk-3.2 → nltk-3.3}/nltk/test/ccg.doctest +376 -395
  230. nltk-3.3/nltk/test/ccg_semantics.doctest +553 -0
  231. {nltk-3.2 → nltk-3.3}/nltk/test/chat80.doctest +234 -234
  232. {nltk-3.2 → nltk-3.3}/nltk/test/childes.doctest +184 -184
  233. {nltk-3.2 → nltk-3.3}/nltk/test/childes_fixt.py +14 -13
  234. {nltk-3.2 → nltk-3.3}/nltk/test/chunk.doctest +373 -373
  235. {nltk-3.2 → nltk-3.3}/nltk/test/classify.doctest +185 -185
  236. {nltk-3.2 → nltk-3.3}/nltk/test/classify_fixt.py +11 -10
  237. nltk-3.3/nltk/test/collections.doctest +20 -0
  238. {nltk-3.2 → nltk-3.3}/nltk/test/collocations.doctest +276 -276
  239. {nltk-3.2 → nltk-3.3}/nltk/test/compat.doctest +134 -138
  240. {nltk-3.2 → nltk-3.3}/nltk/test/compat_fixt.py +9 -8
  241. nltk-3.3/nltk/test/concordance.doctest +55 -0
  242. {nltk-3.2 → nltk-3.3}/nltk/test/corpus.doctest +2200 -2212
  243. {nltk-3.2 → nltk-3.3}/nltk/test/corpus_fixt.py +3 -3
  244. {nltk-3.2 → nltk-3.3}/nltk/test/crubadan.doctest +65 -65
  245. {nltk-3.2 → nltk-3.3}/nltk/test/data.doctest +379 -379
  246. {nltk-3.2 → nltk-3.3}/nltk/test/dependency.doctest +241 -241
  247. {nltk-3.2 → nltk-3.3}/nltk/test/discourse.doctest +546 -546
  248. {nltk-3.2 → nltk-3.3}/nltk/test/discourse_fixt.py +14 -13
  249. {nltk-3.2 → nltk-3.3}/nltk/test/doctest_nose_plugin.py +159 -156
  250. {nltk-3.2 → nltk-3.3}/nltk/test/drt.doctest +517 -517
  251. {nltk-3.2 → nltk-3.3}/nltk/test/featgram.doctest +607 -607
  252. {nltk-3.2 → nltk-3.3}/nltk/test/featstruct.doctest +1229 -1230
  253. {nltk-3.2 → nltk-3.3}/nltk/test/framenet.doctest +288 -239
  254. {nltk-3.2 → nltk-3.3}/nltk/test/generate.doctest +67 -67
  255. {nltk-3.2 → nltk-3.3}/nltk/test/gensim.doctest +140 -141
  256. {nltk-3.2 → nltk-3.3}/nltk/test/gensim_fixt.py +10 -9
  257. {nltk-3.2 → nltk-3.3}/nltk/test/gluesemantics.doctest +384 -386
  258. {nltk-3.2 → nltk-3.3}/nltk/test/gluesemantics_malt.doctest +68 -68
  259. {nltk-3.2 → nltk-3.3}/nltk/test/gluesemantics_malt_fixt.py +12 -11
  260. {nltk-3.2 → nltk-3.3}/nltk/test/grammar.doctest +48 -48
  261. {nltk-3.2 → nltk-3.3}/nltk/test/grammartestsuites.doctest +109 -109
  262. {nltk-3.2 → nltk-3.3}/nltk/test/index.doctest +100 -100
  263. {nltk-3.2 → nltk-3.3}/nltk/test/inference.doctest +534 -534
  264. {nltk-3.2 → nltk-3.3}/nltk/test/inference_fixt.py +12 -11
  265. {nltk-3.2 → nltk-3.3}/nltk/test/internals.doctest +140 -140
  266. {nltk-3.2 → nltk-3.3}/nltk/test/japanese.doctest +48 -48
  267. {nltk-3.2 → nltk-3.3}/nltk/test/logic.doctest +1098 -1098
  268. {nltk-3.2 → nltk-3.3}/nltk/test/metrics.doctest +270 -270
  269. {nltk-3.2 → nltk-3.3}/nltk/test/misc.doctest +118 -118
  270. {nltk-3.2 → nltk-3.3}/nltk/test/nonmonotonic.doctest +286 -286
  271. {nltk-3.2 → nltk-3.3}/nltk/test/nonmonotonic_fixt.py +12 -11
  272. {nltk-3.2 → nltk-3.3}/nltk/test/paice.doctest +35 -35
  273. {nltk-3.2 → nltk-3.3}/nltk/test/parse.doctest +884 -884
  274. {nltk-3.2 → nltk-3.3}/nltk/test/portuguese_en.doctest +565 -565
  275. {nltk-3.2 → nltk-3.3}/nltk/test/portuguese_en_fixt.py +16 -16
  276. {nltk-3.2 → nltk-3.3}/nltk/test/probability.doctest +304 -304
  277. {nltk-3.2 → nltk-3.3}/nltk/test/probability_fixt.py +13 -12
  278. {nltk-3.2 → nltk-3.3}/nltk/test/propbank.doctest +176 -176
  279. {nltk-3.2 → nltk-3.3}/nltk/test/relextract.doctest +263 -263
  280. {nltk-3.2 → nltk-3.3}/nltk/test/resolution.doctest +221 -221
  281. {nltk-3.2 → nltk-3.3}/nltk/test/runtests.py +86 -64
  282. {nltk-3.2 → nltk-3.3}/nltk/test/segmentation_fixt.py +10 -10
  283. {nltk-3.2 → nltk-3.3}/nltk/test/semantics.doctest +665 -665
  284. {nltk-3.2 → nltk-3.3}/nltk/test/semantics_fixt.py +7 -7
  285. {nltk-3.2 → nltk-3.3}/nltk/test/sentiment.doctest +233 -233
  286. {nltk-3.2 → nltk-3.3}/nltk/test/sentiwordnet.doctest +41 -41
  287. {nltk-3.2 → nltk-3.3}/nltk/test/simple.doctest +85 -85
  288. {nltk-3.2 → nltk-3.3}/nltk/test/stem.doctest +78 -78
  289. {nltk-3.2 → nltk-3.3}/nltk/test/tag.doctest +33 -33
  290. {nltk-3.2 → nltk-3.3}/nltk/test/tokenize.doctest +273 -166
  291. {nltk-3.2 → nltk-3.3}/nltk/test/toolbox.doctest +307 -307
  292. {nltk-3.2 → nltk-3.3}/nltk/test/translate.doctest +242 -242
  293. {nltk-3.2 → nltk-3.3}/nltk/test/translate_fixt.py +3 -3
  294. {nltk-3.2 → nltk-3.3}/nltk/test/tree.doctest +1101 -1101
  295. {nltk-3.2 → nltk-3.3}/nltk/test/treeprettyprinter.doctest +127 -127
  296. {nltk-3.2 → nltk-3.3}/nltk/test/treetransforms.doctest +156 -156
  297. nltk-3.3/nltk/test/unit/test_2x_compat.py +63 -0
  298. nltk-3.3/nltk/test/unit/test_aline.py +52 -0
  299. nltk-3.3/nltk/test/unit/test_chunk.py +56 -0
  300. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_classify.py +55 -53
  301. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_collocations.py +88 -79
  302. nltk-3.3/nltk/test/unit/test_concordance.py +98 -0
  303. nltk-3.3/nltk/test/unit/test_corenlp.py +1000 -0
  304. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_corpora.py +200 -183
  305. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_corpus_views.py +45 -45
  306. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_hmm.py +88 -88
  307. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_json2csv_corpus.py +188 -187
  308. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_naivebayes.py +26 -26
  309. nltk-3.3/nltk/test/unit/test_rte_classify.py +86 -0
  310. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_seekable_unicode_stream_reader.py +124 -124
  311. nltk-3.3/nltk/test/unit/test_senna.py +74 -0
  312. nltk-3.3/nltk/test/unit/test_stem.py +121 -0
  313. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_tag.py +20 -20
  314. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_tgrep.py +628 -626
  315. nltk-3.3/nltk/test/unit/test_tokenize.py +168 -0
  316. {nltk-3.2 → nltk-3.3}/nltk/test/unit/test_twitter_auth.py +185 -185
  317. nltk-3.3/nltk/test/unit/test_wordnet.py +134 -0
  318. nltk-3.3/nltk/test/unit/translate/test_bleu.py +244 -0
  319. nltk-3.3/nltk/test/unit/translate/test_gdfa.py +57 -0
  320. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_ibm1.py +75 -75
  321. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_ibm2.py +87 -87
  322. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_ibm3.py +105 -105
  323. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_ibm4.py +126 -126
  324. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_ibm5.py +166 -166
  325. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_ibm_model.py +270 -270
  326. nltk-3.3/nltk/test/unit/translate/test_nist.py +37 -0
  327. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/test_stack_decoder.py +300 -300
  328. {nltk-3.2 → nltk-3.3}/nltk/test/unit/utils.py +41 -41
  329. {nltk-3.2 → nltk-3.3}/nltk/test/util.doctest +48 -48
  330. {nltk-3.2 → nltk-3.3}/nltk/test/wordnet.doctest +595 -581
  331. {nltk-3.2 → nltk-3.3}/nltk/test/wordnet_fixt.py +7 -6
  332. {nltk-3.2 → nltk-3.3}/nltk/test/wordnet_lch.doctest +53 -53
  333. {nltk-3.2 → nltk-3.3}/nltk/test/wsd.doctest +68 -68
  334. {nltk-3.2 → nltk-3.3}/nltk/text.py +663 -610
  335. {nltk-3.2 → nltk-3.3}/nltk/tgrep.py +938 -937
  336. {nltk-3.2 → nltk-3.3}/nltk/tokenize/__init__.py +130 -107
  337. {nltk-3.2 → nltk-3.3}/nltk/tokenize/api.py +77 -78
  338. {nltk-3.2 → nltk-3.3}/nltk/tokenize/casual.py +343 -342
  339. {nltk-3.2 → nltk-3.3}/nltk/tokenize/mwe.py +120 -120
  340. nltk-3.3/nltk/tokenize/nist.py +173 -0
  341. {nltk-3.2 → nltk-3.3}/nltk/tokenize/punkt.py +1620 -1601
  342. {nltk-3.2 → nltk-3.3}/nltk/tokenize/regexp.py +208 -202
  343. nltk-3.3/nltk/tokenize/repp.py +151 -0
  344. {nltk-3.2 → nltk-3.3}/nltk/tokenize/sexpr.py +140 -142
  345. {nltk-3.2 → nltk-3.3}/nltk/tokenize/simple.py +138 -136
  346. {nltk-3.2 → nltk-3.3}/nltk/tokenize/stanford.py +117 -113
  347. nltk-3.3/nltk/tokenize/stanford_segmenter.py +246 -0
  348. {nltk-3.2 → nltk-3.3}/nltk/tokenize/texttiling.py +458 -460
  349. nltk-3.3/nltk/tokenize/toktok.py +155 -0
  350. nltk-3.3/nltk/tokenize/treebank.py +363 -0
  351. nltk-3.3/nltk/tokenize/util.py +261 -0
  352. {nltk-3.2 → nltk-3.3}/nltk/toolbox.py +499 -497
  353. {nltk-3.2 → nltk-3.3}/nltk/translate/__init__.py +24 -24
  354. {nltk-3.2 → nltk-3.3}/nltk/translate/api.py +331 -321
  355. {nltk-3.2 → nltk-3.3}/nltk/translate/bleu_score.py +615 -556
  356. nltk-3.3/nltk/translate/chrf_score.py +137 -0
  357. {nltk-3.2 → nltk-3.3}/nltk/translate/gale_church.py +248 -235
  358. {nltk-3.2 → nltk-3.3}/nltk/translate/gdfa.py +135 -131
  359. nltk-3.3/nltk/translate/gleu_score.py +193 -0
  360. {nltk-3.2 → nltk-3.3}/nltk/translate/ibm1.py +250 -241
  361. {nltk-3.2 → nltk-3.3}/nltk/translate/ibm2.py +308 -308
  362. {nltk-3.2 → nltk-3.3}/nltk/translate/ibm3.py +337 -337
  363. {nltk-3.2 → nltk-3.3}/nltk/translate/ibm4.py +467 -467
  364. {nltk-3.2 → nltk-3.3}/nltk/translate/ibm5.py +639 -639
  365. {nltk-3.2 → nltk-3.3}/nltk/translate/ibm_model.py +536 -533
  366. {nltk-3.2 → nltk-3.3}/nltk/translate/metrics.py +40 -40
  367. nltk-3.3/nltk/translate/nist_score.py +178 -0
  368. {nltk-3.2 → nltk-3.3}/nltk/translate/phrase_based.py +175 -175
  369. {nltk-3.2 → nltk-3.3}/nltk/translate/ribes_score.py +325 -325
  370. {nltk-3.2 → nltk-3.3}/nltk/translate/stack_decoder.py +499 -495
  371. {nltk-3.2 → nltk-3.3}/nltk/tree.py +1605 -1603
  372. {nltk-3.2 → nltk-3.3}/nltk/treeprettyprinter.py +564 -564
  373. {nltk-3.2 → nltk-3.3}/nltk/treetransforms.py +309 -309
  374. {nltk-3.2 → nltk-3.3}/nltk/twitter/__init__.py +28 -28
  375. {nltk-3.2 → nltk-3.3}/nltk/twitter/api.py +141 -136
  376. {nltk-3.2 → nltk-3.3}/nltk/twitter/common.py +264 -258
  377. {nltk-3.2 → nltk-3.3}/nltk/twitter/twitter_demo.py +288 -288
  378. {nltk-3.2 → nltk-3.3}/nltk/twitter/twitterclient.py +530 -530
  379. {nltk-3.2 → nltk-3.3}/nltk/twitter/util.py +147 -146
  380. nltk-3.3/nltk/util.py +751 -0
  381. {nltk-3.2 → nltk-3.3}/nltk/wsd.py +53 -53
  382. {nltk-3.2 → nltk-3.3/nltk.egg-info}/PKG-INFO +13 -5
  383. {nltk-3.2 → nltk-3.3}/nltk.egg-info/SOURCES.txt +26 -3
  384. nltk-3.3/nltk.egg-info/requires.txt +31 -0
  385. {nltk-3.2 → nltk-3.3}/setup.cfg +0 -1
  386. {nltk-3.2 → nltk-3.3}/setup.py +102 -75
  387. nltk-3.2/nltk/VERSION +0 -1
  388. nltk-3.2/nltk/compat.py +0 -680
  389. nltk-3.2/nltk/corpus/reader/pl196x.py +0 -278
  390. nltk-3.2/nltk/corpus/reader/verbnet.py +0 -408
  391. nltk-3.2/nltk/corpus/reader/wordlist.py +0 -37
  392. nltk-3.2/nltk/sentiment/vader_lexicon.txt +0 -7517
  393. nltk-3.2/nltk/six.py +0 -868
  394. nltk-3.2/nltk/stem/porter.py +0 -692
  395. nltk-3.2/nltk/test/unit/test_2x_compat.py +0 -26
  396. nltk-3.2/nltk/test/unit/test_stem.py +0 -36
  397. nltk-3.2/nltk/test/unit/test_tokenize.py +0 -23
  398. nltk-3.2/nltk/test/unit/translate/test_bleu.py +0 -84
  399. nltk-3.2/nltk/tokenize/treebank.py +0 -124
  400. nltk-3.2/nltk/tokenize/util.py +0 -90
  401. nltk-3.2/nltk/util.py +0 -1364
  402. {nltk-3.2 → nltk-3.3}/nltk/test/unit/__init__.py +0 -0
  403. {nltk-3.2 → nltk-3.3}/nltk/test/unit/translate/__init__.py +0 -0
  404. {nltk-3.2 → nltk-3.3}/nltk.egg-info/dependency_links.txt +0 -0
  405. {nltk-3.2 → nltk-3.3}/nltk.egg-info/not-zip-safe +0 -0
  406. {nltk-3.2 → nltk-3.3}/nltk.egg-info/top_level.txt +0 -0
@@ -1,1601 +1,1620 @@
1
- # Natural Language Toolkit: Punkt sentence tokenizer
2
- #
3
- # Copyright (C) 2001-2015 NLTK Project
4
- # Algorithm: Kiss & Strunk (2006)
5
- # Author: Willy <willy@csse.unimelb.edu.au> (original Python port)
6
- # Steven Bird <stevenbird1@gmail.com> (additions)
7
- # Edward Loper <edloper@gmail.com> (rewrite)
8
- # Joel Nothman <jnothman@student.usyd.edu.au> (almost rewrite)
9
- # Arthur Darcet <arthur@darcet.fr> (fixes)
10
- # URL: <http://nltk.org/>
11
- # For license information, see LICENSE.TXT
12
-
13
- r"""
14
- Punkt Sentence Tokenizer
15
-
16
- This tokenizer divides a text into a list of sentences,
17
- by using an unsupervised algorithm to build a model for abbreviation
18
- words, collocations, and words that start sentences. It must be
19
- trained on a large collection of plaintext in the target language
20
- before it can be used.
21
-
22
- The NLTK data package includes a pre-trained Punkt tokenizer for
23
- English.
24
-
25
- >>> import nltk.data
26
- >>> text = '''
27
- ... Punkt knows that the periods in Mr. Smith and Johann S. Bach
28
- ... do not mark sentence boundaries. And sometimes sentences
29
- ... can start with non-capitalized words. i is a good variable
30
- ... name.
31
- ... '''
32
- >>> sent_detector = nltk.data.load('tokenizers/punkt/english.pickle')
33
- >>> print('\n-----\n'.join(sent_detector.tokenize(text.strip())))
34
- Punkt knows that the periods in Mr. Smith and Johann S. Bach
35
- do not mark sentence boundaries.
36
- -----
37
- And sometimes sentences
38
- can start with non-capitalized words.
39
- -----
40
- i is a good variable
41
- name.
42
-
43
- (Note that whitespace from the original text, including newlines, is
44
- retained in the output.)
45
-
46
- Punctuation following sentences is also included by default
47
- (from NLTK 3.0 onwards). It can be excluded with the realign_boundaries
48
- flag.
49
-
50
- >>> text = '''
51
- ... (How does it deal with this parenthesis?) "It should be part of the
52
- ... previous sentence." "(And the same with this one.)" ('And this one!')
53
- ... "('(And (this)) '?)" [(and this. )]
54
- ... '''
55
- >>> print('\n-----\n'.join(
56
- ... sent_detector.tokenize(text.strip())))
57
- (How does it deal with this parenthesis?)
58
- -----
59
- "It should be part of the
60
- previous sentence."
61
- -----
62
- "(And the same with this one.)"
63
- -----
64
- ('And this one!')
65
- -----
66
- "('(And (this)) '?)"
67
- -----
68
- [(and this. )]
69
- >>> print('\n-----\n'.join(
70
- ... sent_detector.tokenize(text.strip(), realign_boundaries=False)))
71
- (How does it deal with this parenthesis?
72
- -----
73
- ) "It should be part of the
74
- previous sentence.
75
- -----
76
- " "(And the same with this one.
77
- -----
78
- )" ('And this one!
79
- -----
80
- ')
81
- "('(And (this)) '?
82
- -----
83
- )" [(and this.
84
- -----
85
- )]
86
-
87
- However, Punkt is designed to learn parameters (a list of abbreviations, etc.)
88
- unsupervised from a corpus similar to the target domain. The pre-packaged models
89
- may therefore be unsuitable: use ``PunktSentenceTokenizer(text)`` to learn
90
- parameters from the given text.
91
-
92
- :class:`.PunktTrainer` learns parameters such as a list of abbreviations
93
- (without supervision) from portions of text. Using a ``PunktTrainer`` directly
94
- allows for incremental training and modification of the hyper-parameters used
95
- to decide what is considered an abbreviation, etc.
96
-
97
- The algorithm for this tokenizer is described in::
98
-
99
- Kiss, Tibor and Strunk, Jan (2006): Unsupervised Multilingual Sentence
100
- Boundary Detection. Computational Linguistics 32: 485-525.
101
- """
102
- from __future__ import print_function, unicode_literals, division
103
-
104
- # TODO: Make orthographic heuristic less susceptible to overtraining
105
- # TODO: Frequent sentence starters optionally exclude always-capitalised words
106
- # FIXME: Problem with ending string with e.g. '!!!' -> '!! !'
107
-
108
- import re
109
- import math
110
- from collections import defaultdict
111
-
112
- from nltk.compat import unicode_repr, python_2_unicode_compatible, string_types
113
- from nltk.probability import FreqDist
114
- from nltk.tokenize.api import TokenizerI
115
-
116
- ######################################################################
117
- #{ Orthographic Context Constants
118
- ######################################################################
119
- # The following constants are used to describe the orthographic
120
- # contexts in which a word can occur. BEG=beginning, MID=middle,
121
- # UNK=unknown, UC=uppercase, LC=lowercase, NC=no case.
122
-
123
- _ORTHO_BEG_UC = 1 << 1
124
- """Orthographic context: beginning of a sentence with upper case."""
125
-
126
- _ORTHO_MID_UC = 1 << 2
127
- """Orthographic context: middle of a sentence with upper case."""
128
-
129
- _ORTHO_UNK_UC = 1 << 3
130
- """Orthographic context: unknown position in a sentence with upper case."""
131
-
132
- _ORTHO_BEG_LC = 1 << 4
133
- """Orthographic context: beginning of a sentence with lower case."""
134
-
135
- _ORTHO_MID_LC = 1 << 5
136
- """Orthographic context: middle of a sentence with lower case."""
137
-
138
- _ORTHO_UNK_LC = 1 << 6
139
- """Orthographic context: unknown position in a sentence with lower case."""
140
-
141
- _ORTHO_UC = _ORTHO_BEG_UC + _ORTHO_MID_UC + _ORTHO_UNK_UC
142
- """Orthographic context: occurs with upper case."""
143
-
144
- _ORTHO_LC = _ORTHO_BEG_LC + _ORTHO_MID_LC + _ORTHO_UNK_LC
145
- """Orthographic context: occurs with lower case."""
146
-
147
- _ORTHO_MAP = {
148
- ('initial', 'upper'): _ORTHO_BEG_UC,
149
- ('internal', 'upper'): _ORTHO_MID_UC,
150
- ('unknown', 'upper'): _ORTHO_UNK_UC,
151
- ('initial', 'lower'): _ORTHO_BEG_LC,
152
- ('internal', 'lower'): _ORTHO_MID_LC,
153
- ('unknown', 'lower'): _ORTHO_UNK_LC,
154
- }
155
- """A map from context position and first-letter case to the
156
- appropriate orthographic context flag."""
157
-
158
- #} (end orthographic context constants)
159
- ######################################################################
160
-
161
- ######################################################################
162
- #{ Decision reasons for debugging
163
- ######################################################################
164
-
165
- REASON_DEFAULT_DECISION = 'default decision'
166
- REASON_KNOWN_COLLOCATION = 'known collocation (both words)'
167
- REASON_ABBR_WITH_ORTHOGRAPHIC_HEURISTIC = 'abbreviation + orthographic heuristic'
168
- REASON_ABBR_WITH_SENTENCE_STARTER = 'abbreviation + frequent sentence starter'
169
- REASON_INITIAL_WITH_ORTHOGRAPHIC_HEURISTIC = 'initial + orthographic heuristic'
170
- REASON_NUMBER_WITH_ORTHOGRAPHIC_HEURISTIC = 'initial + orthographic heuristic'
171
- REASON_INITIAL_WITH_SPECIAL_ORTHOGRAPHIC_HEURISTIC = 'initial + special orthographic heuristic'
172
-
173
- #} (end decision reasons for debugging)
174
- ######################################################################
175
-
176
- ######################################################################
177
- #{ Language-dependent variables
178
- ######################################################################
179
-
180
- class PunktLanguageVars(object):
181
- """
182
- Stores variables, mostly regular expressions, which may be
183
- language-dependent for correct application of the algorithm.
184
- An extension of this class may modify its properties to suit
185
- a language other than English; an instance can then be passed
186
- as an argument to PunktSentenceTokenizer and PunktTrainer
187
- constructors.
188
- """
189
-
190
- __slots__ = ('_re_period_context', '_re_word_tokenizer')
191
-
192
- def __getstate__(self):
193
- # All modifications to the class are performed by inheritance.
194
- # Non-default parameters to be pickled must be defined in the inherited
195
- # class.
196
- return 1
197
-
198
- def __setstate__(self, state):
199
- return 1
200
-
201
- sent_end_chars = ('.', '?', '!')
202
- """Characters which are candidates for sentence boundaries"""
203
-
204
- @property
205
- def _re_sent_end_chars(self):
206
- return '[%s]' % re.escape(''.join(self.sent_end_chars))
207
-
208
- internal_punctuation = ',:;' # might want to extend this..
209
- """sentence internal punctuation, which indicates an abbreviation if
210
- preceded by a period-final token."""
211
-
212
- re_boundary_realignment = re.compile(r'["\')\]}]+?(?:\s+|(?=--)|$)',
213
- re.MULTILINE)
214
- """Used to realign punctuation that should be included in a sentence
215
- although it follows the period (or ?, !)."""
216
-
217
- _re_word_start = r"[^\(\"\`{\[:;&\#\*@\)}\]\-,]"
218
- """Excludes some characters from starting word tokens"""
219
-
220
- _re_non_word_chars = r"(?:[?!)\";}\]\*:@\'\({\[])"
221
- """Characters that cannot appear within words"""
222
-
223
- _re_multi_char_punct = r"(?:\-{2,}|\.{2,}|(?:\.\s){2,}\.)"
224
- """Hyphen and ellipsis are multi-character punctuation"""
225
-
226
- _word_tokenize_fmt = r'''(
227
- %(MultiChar)s
228
- |
229
- (?=%(WordStart)s)\S+? # Accept word characters until end is found
230
- (?= # Sequences marking a word's end
231
- \s| # White-space
232
- $| # End-of-string
233
- %(NonWord)s|%(MultiChar)s| # Punctuation
234
- ,(?=$|\s|%(NonWord)s|%(MultiChar)s) # Comma if at end of word
235
- )
236
- |
237
- \S
238
- )'''
239
- """Format of a regular expression to split punctuation from words,
240
- excluding period."""
241
-
242
- def _word_tokenizer_re(self):
243
- """Compiles and returns a regular expression for word tokenization"""
244
- try:
245
- return self._re_word_tokenizer
246
- except AttributeError:
247
- self._re_word_tokenizer = re.compile(
248
- self._word_tokenize_fmt %
249
- {
250
- 'NonWord': self._re_non_word_chars,
251
- 'MultiChar': self._re_multi_char_punct,
252
- 'WordStart': self._re_word_start,
253
- },
254
- re.UNICODE | re.VERBOSE
255
- )
256
- return self._re_word_tokenizer
257
-
258
- def word_tokenize(self, s):
259
- """Tokenize a string to split off punctuation other than periods"""
260
- return self._word_tokenizer_re().findall(s)
261
-
262
- _period_context_fmt = r"""
263
- \S* # some word material
264
- %(SentEndChars)s # a potential sentence ending
265
- (?=(?P<after_tok>
266
- %(NonWord)s # either other punctuation
267
- |
268
- \s+(?P<next_tok>\S+) # or whitespace and some other token
269
- ))"""
270
- """Format of a regular expression to find contexts including possible
271
- sentence boundaries. Matches token which the possible sentence boundary
272
- ends, and matches the following token within a lookahead expression."""
273
-
274
- def period_context_re(self):
275
- """Compiles and returns a regular expression to find contexts
276
- including possible sentence boundaries."""
277
- try:
278
- return self._re_period_context
279
- except:
280
- self._re_period_context = re.compile(
281
- self._period_context_fmt %
282
- {
283
- 'NonWord': self._re_non_word_chars,
284
- 'SentEndChars': self._re_sent_end_chars,
285
- },
286
- re.UNICODE | re.VERBOSE)
287
- return self._re_period_context
288
-
289
-
290
- _re_non_punct = re.compile(r'[^\W\d]', re.UNICODE)
291
- """Matches token types that are not merely punctuation. (Types for
292
- numeric tokens are changed to ##number## and hence contain alpha.)"""
293
-
294
- #}
295
- ######################################################################
296
-
297
-
298
-
299
- #////////////////////////////////////////////////////////////
300
- #{ Helper Functions
301
- #////////////////////////////////////////////////////////////
302
-
303
- def _pair_iter(it):
304
- """
305
- Yields pairs of tokens from the given iterator such that each input
306
- token will appear as the first element in a yielded tuple. The last
307
- pair will have None as its second element.
308
- """
309
- it = iter(it)
310
- prev = next(it)
311
- for el in it:
312
- yield (prev, el)
313
- prev = el
314
- yield (prev, None)
315
-
316
- ######################################################################
317
- #{ Punkt Parameters
318
- ######################################################################
319
-
320
- class PunktParameters(object):
321
- """Stores data used to perform sentence boundary detection with Punkt."""
322
-
323
- def __init__(self):
324
- self.abbrev_types = set()
325
- """A set of word types for known abbreviations."""
326
-
327
- self.collocations = set()
328
- """A set of word type tuples for known common collocations
329
- where the first word ends in a period. E.g., ('S.', 'Bach')
330
- is a common collocation in a text that discusses 'Johann
331
- S. Bach'. These count as negative evidence for sentence
332
- boundaries."""
333
-
334
- self.sent_starters = set()
335
- """A set of word types for words that often appear at the
336
- beginning of sentences."""
337
-
338
- self.ortho_context = defaultdict(int)
339
- """A dictionary mapping word types to the set of orthographic
340
- contexts that word type appears in. Contexts are represented
341
- by adding orthographic context flags: ..."""
342
-
343
- def clear_abbrevs(self):
344
- self.abbrev_types = set()
345
-
346
- def clear_collocations(self):
347
- self.collocations = set()
348
-
349
- def clear_sent_starters(self):
350
- self.sent_starters = set()
351
-
352
- def clear_ortho_context(self):
353
- self.ortho_context = defaultdict(int)
354
-
355
- def add_ortho_context(self, typ, flag):
356
- self.ortho_context[typ] |= flag
357
-
358
- def _debug_ortho_context(self, typ):
359
- c = self.ortho_context[typ]
360
- if c & _ORTHO_BEG_UC:
361
- yield 'BEG-UC'
362
- if c & _ORTHO_MID_UC:
363
- yield 'MID-UC'
364
- if c & _ORTHO_UNK_UC:
365
- yield 'UNK-UC'
366
- if c & _ORTHO_BEG_LC:
367
- yield 'BEG-LC'
368
- if c & _ORTHO_MID_LC:
369
- yield 'MID-LC'
370
- if c & _ORTHO_UNK_LC:
371
- yield 'UNK-LC'
372
-
373
- ######################################################################
374
- #{ PunktToken
375
- ######################################################################
376
-
377
- @python_2_unicode_compatible
378
- class PunktToken(object):
379
- """Stores a token of text with annotations produced during
380
- sentence boundary detection."""
381
-
382
- _properties = [
383
- 'parastart', 'linestart',
384
- 'sentbreak', 'abbr', 'ellipsis'
385
- ]
386
- __slots__ = ['tok', 'type', 'period_final'] + _properties
387
-
388
- def __init__(self, tok, **params):
389
- self.tok = tok
390
- self.type = self._get_type(tok)
391
- self.period_final = tok.endswith('.')
392
-
393
- for p in self._properties:
394
- setattr(self, p, None)
395
- for k in params:
396
- setattr(self, k, params[k])
397
-
398
- #////////////////////////////////////////////////////////////
399
- #{ Regular expressions for properties
400
- #////////////////////////////////////////////////////////////
401
- # Note: [A-Za-z] is approximated by [^\W\d] in the general case.
402
- _RE_ELLIPSIS = re.compile(r'\.\.+$')
403
- _RE_NUMERIC = re.compile(r'^-?[\.,]?\d[\d,\.-]*\.?$')
404
- _RE_INITIAL = re.compile(r'[^\W\d]\.$', re.UNICODE)
405
- _RE_ALPHA = re.compile(r'[^\W\d]+$', re.UNICODE)
406
-
407
- #////////////////////////////////////////////////////////////
408
- #{ Derived properties
409
- #////////////////////////////////////////////////////////////
410
-
411
- def _get_type(self, tok):
412
- """Returns a case-normalized representation of the token."""
413
- return self._RE_NUMERIC.sub('##number##', tok.lower())
414
-
415
- @property
416
- def type_no_period(self):
417
- """
418
- The type with its final period removed if it has one.
419
- """
420
- if len(self.type) > 1 and self.type[-1] == '.':
421
- return self.type[:-1]
422
- return self.type
423
-
424
- @property
425
- def type_no_sentperiod(self):
426
- """
427
- The type with its final period removed if it is marked as a
428
- sentence break.
429
- """
430
- if self.sentbreak:
431
- return self.type_no_period
432
- return self.type
433
-
434
- @property
435
- def first_upper(self):
436
- """True if the token's first character is uppercase."""
437
- return self.tok[0].isupper()
438
-
439
- @property
440
- def first_lower(self):
441
- """True if the token's first character is lowercase."""
442
- return self.tok[0].islower()
443
-
444
- @property
445
- def first_case(self):
446
- if self.first_lower:
447
- return 'lower'
448
- elif self.first_upper:
449
- return 'upper'
450
- return 'none'
451
-
452
- @property
453
- def is_ellipsis(self):
454
- """True if the token text is that of an ellipsis."""
455
- return self._RE_ELLIPSIS.match(self.tok)
456
-
457
- @property
458
- def is_number(self):
459
- """True if the token text is that of a number."""
460
- return self.type.startswith('##number##')
461
-
462
- @property
463
- def is_initial(self):
464
- """True if the token text is that of an initial."""
465
- return self._RE_INITIAL.match(self.tok)
466
-
467
- @property
468
- def is_alpha(self):
469
- """True if the token text is all alphabetic."""
470
- return self._RE_ALPHA.match(self.tok)
471
-
472
- @property
473
- def is_non_punct(self):
474
- """True if the token is either a number or is alphabetic."""
475
- return _re_non_punct.search(self.type)
476
-
477
- #////////////////////////////////////////////////////////////
478
- #{ String representation
479
- #////////////////////////////////////////////////////////////
480
-
481
- def __repr__(self):
482
- """
483
- A string representation of the token that can reproduce it
484
- with eval(), which lists all the token's non-default
485
- annotations.
486
- """
487
- typestr = (' type=%s,' % unicode_repr(self.type)
488
- if self.type != self.tok else '')
489
-
490
- propvals = ', '.join(
491
- '%s=%s' % (p, unicode_repr(getattr(self, p)))
492
- for p in self._properties
493
- if getattr(self, p)
494
- )
495
-
496
- return '%s(%s,%s %s)' % (self.__class__.__name__,
497
- unicode_repr(self.tok), typestr, propvals)
498
-
499
- def __str__(self):
500
- """
501
- A string representation akin to that used by Kiss and Strunk.
502
- """
503
- res = self.tok
504
- if self.abbr:
505
- res += '<A>'
506
- if self.ellipsis:
507
- res += '<E>'
508
- if self.sentbreak:
509
- res += '<S>'
510
- return res
511
-
512
- ######################################################################
513
- #{ Punkt base class
514
- ######################################################################
515
-
516
- class PunktBaseClass(object):
517
- """
518
- Includes common components of PunktTrainer and PunktSentenceTokenizer.
519
- """
520
-
521
- def __init__(self, lang_vars=PunktLanguageVars(), token_cls=PunktToken,
522
- params=PunktParameters()):
523
- self._params = params
524
- self._lang_vars = lang_vars
525
- self._Token = token_cls
526
- """The collection of parameters that determines the behavior
527
- of the punkt tokenizer."""
528
-
529
- #////////////////////////////////////////////////////////////
530
- #{ Word tokenization
531
- #////////////////////////////////////////////////////////////
532
-
533
- def _tokenize_words(self, plaintext):
534
- """
535
- Divide the given text into tokens, using the punkt word
536
- segmentation regular expression, and generate the resulting list
537
- of tokens augmented as three-tuples with two boolean values for whether
538
- the given token occurs at the start of a paragraph or a new line,
539
- respectively.
540
- """
541
- parastart = False
542
- for line in plaintext.split('\n'):
543
- if line.strip():
544
- line_toks = iter(self._lang_vars.word_tokenize(line))
545
-
546
- yield self._Token(next(line_toks),
547
- parastart=parastart, linestart=True)
548
- parastart = False
549
-
550
- for t in line_toks:
551
- yield self._Token(t)
552
- else:
553
- parastart = True
554
-
555
-
556
- #////////////////////////////////////////////////////////////
557
- #{ Annotation Procedures
558
- #////////////////////////////////////////////////////////////
559
-
560
- def _annotate_first_pass(self, tokens):
561
- """
562
- Perform the first pass of annotation, which makes decisions
563
- based purely based on the word type of each word:
564
-
565
- - '?', '!', and '.' are marked as sentence breaks.
566
- - sequences of two or more periods are marked as ellipsis.
567
- - any word ending in '.' that's a known abbreviation is
568
- marked as an abbreviation.
569
- - any other word ending in '.' is marked as a sentence break.
570
-
571
- Return these annotations as a tuple of three sets:
572
-
573
- - sentbreak_toks: The indices of all sentence breaks.
574
- - abbrev_toks: The indices of all abbreviations.
575
- - ellipsis_toks: The indices of all ellipsis marks.
576
- """
577
- for aug_tok in tokens:
578
- self._first_pass_annotation(aug_tok)
579
- yield aug_tok
580
-
581
- def _first_pass_annotation(self, aug_tok):
582
- """
583
- Performs type-based annotation on a single token.
584
- """
585
-
586
- tok = aug_tok.tok
587
-
588
- if tok in self._lang_vars.sent_end_chars:
589
- aug_tok.sentbreak = True
590
- elif aug_tok.is_ellipsis:
591
- aug_tok.ellipsis = True
592
- elif aug_tok.period_final and not tok.endswith('..'):
593
- if (tok[:-1].lower() in self._params.abbrev_types or
594
- tok[:-1].lower().split('-')[-1] in self._params.abbrev_types):
595
-
596
- aug_tok.abbr = True
597
- else:
598
- aug_tok.sentbreak = True
599
-
600
- return
601
-
602
- ######################################################################
603
- #{ Punkt Trainer
604
- ######################################################################
605
-
606
-
607
- class PunktTrainer(PunktBaseClass):
608
- """Learns parameters used in Punkt sentence boundary detection."""
609
-
610
- def __init__(self, train_text=None, verbose=False,
611
- lang_vars=PunktLanguageVars(), token_cls=PunktToken):
612
-
613
- PunktBaseClass.__init__(self, lang_vars=lang_vars,
614
- token_cls=token_cls)
615
-
616
- self._type_fdist = FreqDist()
617
- """A frequency distribution giving the frequency of each
618
- case-normalized token type in the training data."""
619
-
620
- self._num_period_toks = 0
621
- """The number of words ending in period in the training data."""
622
-
623
- self._collocation_fdist = FreqDist()
624
- """A frequency distribution giving the frequency of all
625
- bigrams in the training data where the first word ends in a
626
- period. Bigrams are encoded as tuples of word types.
627
- Especially common collocations are extracted from this
628
- frequency distribution, and stored in
629
- ``_params``.``collocations <PunktParameters.collocations>``."""
630
-
631
- self._sent_starter_fdist = FreqDist()
632
- """A frequency distribution giving the frequency of all words
633
- that occur at the training data at the beginning of a sentence
634
- (after the first pass of annotation). Especially common
635
- sentence starters are extracted from this frequency
636
- distribution, and stored in ``_params.sent_starters``.
637
- """
638
-
639
- self._sentbreak_count = 0
640
- """The total number of sentence breaks identified in training, used for
641
- calculating the frequent sentence starter heuristic."""
642
-
643
- self._finalized = True
644
- """A flag as to whether the training has been finalized by finding
645
- collocations and sentence starters, or whether finalize_training()
646
- still needs to be called."""
647
-
648
- if train_text:
649
- self.train(train_text, verbose, finalize=True)
650
-
651
- def get_params(self):
652
- """
653
- Calculates and returns parameters for sentence boundary detection as
654
- derived from training."""
655
- if not self._finalized:
656
- self.finalize_training()
657
- return self._params
658
-
659
- #////////////////////////////////////////////////////////////
660
- #{ Customization Variables
661
- #////////////////////////////////////////////////////////////
662
-
663
- ABBREV = 0.3
664
- """cut-off value whether a 'token' is an abbreviation"""
665
-
666
- IGNORE_ABBREV_PENALTY = False
667
- """allows the disabling of the abbreviation penalty heuristic, which
668
- exponentially disadvantages words that are found at times without a
669
- final period."""
670
-
671
- ABBREV_BACKOFF = 5
672
- """upper cut-off for Mikheev's(2002) abbreviation detection algorithm"""
673
-
674
- COLLOCATION = 7.88
675
- """minimal log-likelihood value that two tokens need to be considered
676
- as a collocation"""
677
-
678
- SENT_STARTER = 30
679
- """minimal log-likelihood value that a token requires to be considered
680
- as a frequent sentence starter"""
681
-
682
- INCLUDE_ALL_COLLOCS = False
683
- """this includes as potential collocations all word pairs where the first
684
- word ends in a period. It may be useful in corpora where there is a lot
685
- of variation that makes abbreviations like Mr difficult to identify."""
686
-
687
- INCLUDE_ABBREV_COLLOCS = False
688
- """this includes as potential collocations all word pairs where the first
689
- word is an abbreviation. Such collocations override the orthographic
690
- heuristic, but not the sentence starter heuristic. This is overridden by
691
- INCLUDE_ALL_COLLOCS, and if both are false, only collocations with initials
692
- and ordinals are considered."""
693
- """"""
694
-
695
- MIN_COLLOC_FREQ = 1
696
- """this sets a minimum bound on the number of times a bigram needs to
697
- appear before it can be considered a collocation, in addition to log
698
- likelihood statistics. This is useful when INCLUDE_ALL_COLLOCS is True."""
699
-
700
- #////////////////////////////////////////////////////////////
701
- #{ Training..
702
- #////////////////////////////////////////////////////////////
703
-
704
- def train(self, text, verbose=False, finalize=True):
705
- """
706
- Collects training data from a given text. If finalize is True, it
707
- will determine all the parameters for sentence boundary detection. If
708
- not, this will be delayed until get_params() or finalize_training() is
709
- called. If verbose is True, abbreviations found will be listed.
710
- """
711
- # Break the text into tokens; record which token indices correspond to
712
- # line starts and paragraph starts; and determine their types.
713
- self._train_tokens(self._tokenize_words(text), verbose)
714
- if finalize:
715
- self.finalize_training(verbose)
716
-
717
- def train_tokens(self, tokens, verbose=False, finalize=True):
718
- """
719
- Collects training data from a given list of tokens.
720
- """
721
- self._train_tokens((self._Token(t) for t in tokens), verbose)
722
- if finalize:
723
- self.finalize_training(verbose)
724
-
725
- def _train_tokens(self, tokens, verbose):
726
- self._finalized = False
727
-
728
- # Ensure tokens are a list
729
- tokens = list(tokens)
730
-
731
- # Find the frequency of each case-normalized type. (Don't
732
- # strip off final periods.) Also keep track of the number of
733
- # tokens that end in periods.
734
- for aug_tok in tokens:
735
- self._type_fdist[aug_tok.type] += 1
736
- if aug_tok.period_final:
737
- self._num_period_toks += 1
738
-
739
- # Look for new abbreviations, and for types that no longer are
740
- unique_types = self._unique_types(tokens)
741
- for abbr, score, is_add in self._reclassify_abbrev_types(unique_types):
742
- if score >= self.ABBREV:
743
- if is_add:
744
- self._params.abbrev_types.add(abbr)
745
- if verbose:
746
- print((' Abbreviation: [%6.4f] %s' %
747
- (score, abbr)))
748
- else:
749
- if not is_add:
750
- self._params.abbrev_types.remove(abbr)
751
- if verbose:
752
- print((' Removed abbreviation: [%6.4f] %s' %
753
- (score, abbr)))
754
-
755
- # Make a preliminary pass through the document, marking likely
756
- # sentence breaks, abbreviations, and ellipsis tokens.
757
- tokens = list(self._annotate_first_pass(tokens))
758
-
759
- # Check what contexts each word type can appear in, given the
760
- # case of its first letter.
761
- self._get_orthography_data(tokens)
762
-
763
- # We need total number of sentence breaks to find sentence starters
764
- self._sentbreak_count += self._get_sentbreak_count(tokens)
765
-
766
- # The remaining heuristics relate to pairs of tokens where the first
767
- # ends in a period.
768
- for aug_tok1, aug_tok2 in _pair_iter(tokens):
769
- if not aug_tok1.period_final or not aug_tok2:
770
- continue
771
-
772
- # Is the first token a rare abbreviation?
773
- if self._is_rare_abbrev_type(aug_tok1, aug_tok2):
774
- self._params.abbrev_types.add(aug_tok1.type_no_period)
775
- if verbose:
776
- print((' Rare Abbrev: %s' % aug_tok1.type))
777
-
778
- # Does second token have a high likelihood of starting a sentence?
779
- if self._is_potential_sent_starter(aug_tok2, aug_tok1):
780
- self._sent_starter_fdist[aug_tok2.type] += 1
781
-
782
- # Is this bigram a potential collocation?
783
- if self._is_potential_collocation(aug_tok1, aug_tok2):
784
- self._collocation_fdist[
785
- (aug_tok1.type_no_period, aug_tok2.type_no_sentperiod)] += 1
786
-
787
- def _unique_types(self, tokens):
788
- return set(aug_tok.type for aug_tok in tokens)
789
-
790
- def finalize_training(self, verbose=False):
791
- """
792
- Uses data that has been gathered in training to determine likely
793
- collocations and sentence starters.
794
- """
795
- self._params.clear_sent_starters()
796
- for typ, ll in self._find_sent_starters():
797
- self._params.sent_starters.add(typ)
798
- if verbose:
799
- print((' Sent Starter: [%6.4f] %r' % (ll, typ)))
800
-
801
- self._params.clear_collocations()
802
- for (typ1, typ2), ll in self._find_collocations():
803
- self._params.collocations.add( (typ1,typ2) )
804
- if verbose:
805
- print((' Collocation: [%6.4f] %r+%r' %
806
- (ll, typ1, typ2)))
807
-
808
- self._finalized = True
809
-
810
- #////////////////////////////////////////////////////////////
811
- #{ Overhead reduction
812
- #////////////////////////////////////////////////////////////
813
-
814
- def freq_threshold(self, ortho_thresh=2, type_thresh=2, colloc_thres=2,
815
- sentstart_thresh=2):
816
- """
817
- Allows memory use to be reduced after much training by removing data
818
- about rare tokens that are unlikely to have a statistical effect with
819
- further training. Entries occurring above the given thresholds will be
820
- retained.
821
- """
822
- if ortho_thresh > 1:
823
- old_oc = self._params.ortho_context
824
- self._params.clear_ortho_context()
825
- for tok in self._type_fdist:
826
- count = self._type_fdist[tok]
827
- if count >= ortho_thresh:
828
- self._params.ortho_context[tok] = old_oc[tok]
829
-
830
- self._type_fdist = self._freq_threshold(self._type_fdist, type_thresh)
831
- self._collocation_fdist = self._freq_threshold(
832
- self._collocation_fdist, colloc_thres)
833
- self._sent_starter_fdist = self._freq_threshold(
834
- self._sent_starter_fdist, sentstart_thresh)
835
-
836
- def _freq_threshold(self, fdist, threshold):
837
- """
838
- Returns a FreqDist containing only data with counts below a given
839
- threshold, as well as a mapping (None -> count_removed).
840
- """
841
- # We assume that there is more data below the threshold than above it
842
- # and so create a new FreqDist rather than working in place.
843
- res = FreqDist()
844
- num_removed = 0
845
- for tok in fdist:
846
- count = fdist[tok]
847
- if count < threshold:
848
- num_removed += 1
849
- else:
850
- res[tok] += count
851
- res[None] += num_removed
852
- return res
853
-
854
- #////////////////////////////////////////////////////////////
855
- #{ Orthographic data
856
- #////////////////////////////////////////////////////////////
857
-
858
- def _get_orthography_data(self, tokens):
859
- """
860
- Collect information about whether each token type occurs
861
- with different case patterns (i) overall, (ii) at
862
- sentence-initial positions, and (iii) at sentence-internal
863
- positions.
864
- """
865
- # 'initial' or 'internal' or 'unknown'
866
- context = 'internal'
867
- tokens = list(tokens)
868
-
869
- for aug_tok in tokens:
870
- # If we encounter a paragraph break, then it's a good sign
871
- # that it's a sentence break. But err on the side of
872
- # caution (by not positing a sentence break) if we just
873
- # saw an abbreviation.
874
- if aug_tok.parastart and context != 'unknown':
875
- context = 'initial'
876
-
877
- # If we're at the beginning of a line, then we can't decide
878
- # between 'internal' and 'initial'.
879
- if aug_tok.linestart and context == 'internal':
880
- context = 'unknown'
881
-
882
- # Find the case-normalized type of the token. If it's a
883
- # sentence-final token, strip off the period.
884
- typ = aug_tok.type_no_sentperiod
885
-
886
- # Update the orthographic context table.
887
- flag = _ORTHO_MAP.get((context, aug_tok.first_case), 0)
888
- if flag:
889
- self._params.add_ortho_context(typ, flag)
890
-
891
- # Decide whether the next word is at a sentence boundary.
892
- if aug_tok.sentbreak:
893
- if not (aug_tok.is_number or aug_tok.is_initial):
894
- context = 'initial'
895
- else:
896
- context = 'unknown'
897
- elif aug_tok.ellipsis or aug_tok.abbr:
898
- context = 'unknown'
899
- else:
900
- context = 'internal'
901
-
902
- #////////////////////////////////////////////////////////////
903
- #{ Abbreviations
904
- #////////////////////////////////////////////////////////////
905
-
906
- def _reclassify_abbrev_types(self, types):
907
- """
908
- (Re)classifies each given token if
909
- - it is period-final and not a known abbreviation; or
910
- - it is not period-final and is otherwise a known abbreviation
911
- by checking whether its previous classification still holds according
912
- to the heuristics of section 3.
913
- Yields triples (abbr, score, is_add) where abbr is the type in question,
914
- score is its log-likelihood with penalties applied, and is_add specifies
915
- whether the present type is a candidate for inclusion or exclusion as an
916
- abbreviation, such that:
917
- - (is_add and score >= 0.3) suggests a new abbreviation; and
918
- - (not is_add and score < 0.3) suggests excluding an abbreviation.
919
- """
920
- # (While one could recalculate abbreviations from all .-final tokens at
921
- # every iteration, in cases requiring efficiency, the number of tokens
922
- # in the present training document will be much less.)
923
-
924
- for typ in types:
925
- # Check some basic conditions, to rule out words that are
926
- # clearly not abbrev_types.
927
- if not _re_non_punct.search(typ) or typ == '##number##':
928
- continue
929
-
930
- if typ.endswith('.'):
931
- if typ in self._params.abbrev_types:
932
- continue
933
- typ = typ[:-1]
934
- is_add = True
935
- else:
936
- if typ not in self._params.abbrev_types:
937
- continue
938
- is_add = False
939
-
940
- # Count how many periods & nonperiods are in the
941
- # candidate.
942
- num_periods = typ.count('.') + 1
943
- num_nonperiods = len(typ) - num_periods + 1
944
-
945
- # Let <a> be the candidate without the period, and <b>
946
- # be the period. Find a log likelihood ratio that
947
- # indicates whether <ab> occurs as a single unit (high
948
- # value of ll), or as two independent units <a> and
949
- # <b> (low value of ll).
950
- count_with_period = self._type_fdist[typ + '.']
951
- count_without_period = self._type_fdist[typ]
952
- ll = self._dunning_log_likelihood(
953
- count_with_period + count_without_period,
954
- self._num_period_toks, count_with_period,
955
- self._type_fdist.N())
956
-
957
- # Apply three scaling factors to 'tweak' the basic log
958
- # likelihood ratio:
959
- # F_length: long word -> less likely to be an abbrev
960
- # F_periods: more periods -> more likely to be an abbrev
961
- # F_penalty: penalize occurrences w/o a period
962
- f_length = math.exp(-num_nonperiods)
963
- f_periods = num_periods
964
- f_penalty = (int(self.IGNORE_ABBREV_PENALTY)
965
- or math.pow(num_nonperiods, -count_without_period))
966
- score = ll * f_length * f_periods * f_penalty
967
-
968
- yield typ, score, is_add
969
-
970
- def find_abbrev_types(self):
971
- """
972
- Recalculates abbreviations given type frequencies, despite no prior
973
- determination of abbreviations.
974
- This fails to include abbreviations otherwise found as "rare".
975
- """
976
- self._params.clear_abbrevs()
977
- tokens = (typ for typ in self._type_fdist if typ and typ.endswith('.'))
978
- for abbr, score, is_add in self._reclassify_abbrev_types(tokens):
979
- if score >= self.ABBREV:
980
- self._params.abbrev_types.add(abbr)
981
-
982
- # This function combines the work done by the original code's
983
- # functions `count_orthography_context`, `get_orthography_count`,
984
- # and `get_rare_abbreviations`.
985
- def _is_rare_abbrev_type(self, cur_tok, next_tok):
986
- """
987
- A word type is counted as a rare abbreviation if...
988
- - it's not already marked as an abbreviation
989
- - it occurs fewer than ABBREV_BACKOFF times
990
- - either it is followed by a sentence-internal punctuation
991
- mark, *or* it is followed by a lower-case word that
992
- sometimes appears with upper case, but never occurs with
993
- lower case at the beginning of sentences.
994
- """
995
- if cur_tok.abbr or not cur_tok.sentbreak:
996
- return False
997
-
998
- # Find the case-normalized type of the token. If it's
999
- # a sentence-final token, strip off the period.
1000
- typ = cur_tok.type_no_sentperiod
1001
-
1002
- # Proceed only if the type hasn't been categorized as an
1003
- # abbreviation already, and is sufficiently rare...
1004
- count = self._type_fdist[typ] + self._type_fdist[typ[:-1]]
1005
- if (typ in self._params.abbrev_types or count >= self.ABBREV_BACKOFF):
1006
- return False
1007
-
1008
- # Record this token as an abbreviation if the next
1009
- # token is a sentence-internal punctuation mark.
1010
- # [XX] :1 or check the whole thing??
1011
- if next_tok.tok[:1] in self._lang_vars.internal_punctuation:
1012
- return True
1013
-
1014
- # Record this type as an abbreviation if the next
1015
- # token... (i) starts with a lower case letter,
1016
- # (ii) sometimes occurs with an uppercase letter,
1017
- # and (iii) never occus with an uppercase letter
1018
- # sentence-internally.
1019
- # [xx] should the check for (ii) be modified??
1020
- elif next_tok.first_lower:
1021
- typ2 = next_tok.type_no_sentperiod
1022
- typ2ortho_context = self._params.ortho_context[typ2]
1023
- if ( (typ2ortho_context & _ORTHO_BEG_UC) and
1024
- not (typ2ortho_context & _ORTHO_MID_UC) ):
1025
- return True
1026
-
1027
- #////////////////////////////////////////////////////////////
1028
- #{ Log Likelihoods
1029
- #////////////////////////////////////////////////////////////
1030
-
1031
- # helper for _reclassify_abbrev_types:
1032
- @staticmethod
1033
- def _dunning_log_likelihood(count_a, count_b, count_ab, N):
1034
- """
1035
- A function that calculates the modified Dunning log-likelihood
1036
- ratio scores for abbreviation candidates. The details of how
1037
- this works is available in the paper.
1038
- """
1039
- p1 = count_b / N
1040
- p2 = 0.99
1041
-
1042
- null_hypo = (count_ab * math.log(p1) +
1043
- (count_a - count_ab) * math.log(1.0 - p1))
1044
- alt_hypo = (count_ab * math.log(p2) +
1045
- (count_a - count_ab) * math.log(1.0 - p2))
1046
-
1047
- likelihood = null_hypo - alt_hypo
1048
-
1049
- return (-2.0 * likelihood)
1050
-
1051
- @staticmethod
1052
- def _col_log_likelihood(count_a, count_b, count_ab, N):
1053
- """
1054
- A function that will just compute log-likelihood estimate, in
1055
- the original paper it's described in algorithm 6 and 7.
1056
-
1057
- This *should* be the original Dunning log-likelihood values,
1058
- unlike the previous log_l function where it used modified
1059
- Dunning log-likelihood values
1060
- """
1061
- import math
1062
-
1063
- p = count_b / N
1064
- p1 = count_ab / count_a
1065
- p2 = (count_b - count_ab) / (N - count_a)
1066
-
1067
- summand1 = (count_ab * math.log(p) +
1068
- (count_a - count_ab) * math.log(1.0 - p))
1069
-
1070
- summand2 = ((count_b - count_ab) * math.log(p) +
1071
- (N - count_a - count_b + count_ab) * math.log(1.0 - p))
1072
-
1073
- if count_a == count_ab:
1074
- summand3 = 0
1075
- else:
1076
- summand3 = (count_ab * math.log(p1) +
1077
- (count_a - count_ab) * math.log(1.0 - p1))
1078
-
1079
- if count_b == count_ab:
1080
- summand4 = 0
1081
- else:
1082
- summand4 = ((count_b - count_ab) * math.log(p2) +
1083
- (N - count_a - count_b + count_ab) * math.log(1.0 - p2))
1084
-
1085
- likelihood = summand1 + summand2 - summand3 - summand4
1086
-
1087
- return (-2.0 * likelihood)
1088
-
1089
- #////////////////////////////////////////////////////////////
1090
- #{ Collocation Finder
1091
- #////////////////////////////////////////////////////////////
1092
-
1093
- def _is_potential_collocation(self, aug_tok1, aug_tok2):
1094
- """
1095
- Returns True if the pair of tokens may form a collocation given
1096
- log-likelihood statistics.
1097
- """
1098
- return ((self.INCLUDE_ALL_COLLOCS or
1099
- (self.INCLUDE_ABBREV_COLLOCS and aug_tok1.abbr) or
1100
- (aug_tok1.sentbreak and
1101
- (aug_tok1.is_number or aug_tok1.is_initial)))
1102
- and aug_tok1.is_non_punct
1103
- and aug_tok2.is_non_punct)
1104
-
1105
- def _find_collocations(self):
1106
- """
1107
- Generates likely collocations and their log-likelihood.
1108
- """
1109
- for types in self._collocation_fdist:
1110
- try:
1111
- typ1, typ2 = types
1112
- except TypeError:
1113
- # types may be None after calling freq_threshold()
1114
- continue
1115
- if typ2 in self._params.sent_starters:
1116
- continue
1117
-
1118
- col_count = self._collocation_fdist[types]
1119
- typ1_count = self._type_fdist[typ1]+self._type_fdist[typ1+'.']
1120
- typ2_count = self._type_fdist[typ2]+self._type_fdist[typ2+'.']
1121
- if (typ1_count > 1 and typ2_count > 1
1122
- and self.MIN_COLLOC_FREQ <
1123
- col_count <= min(typ1_count, typ2_count)):
1124
-
1125
- ll = self._col_log_likelihood(typ1_count, typ2_count,
1126
- col_count, self._type_fdist.N())
1127
- # Filter out the not-so-collocative
1128
- if (ll >= self.COLLOCATION and
1129
- (self._type_fdist.N()/typ1_count >
1130
- typ2_count/col_count)):
1131
- yield (typ1, typ2), ll
1132
-
1133
- #////////////////////////////////////////////////////////////
1134
- #{ Sentence-Starter Finder
1135
- #////////////////////////////////////////////////////////////
1136
-
1137
- def _is_potential_sent_starter(self, cur_tok, prev_tok):
1138
- """
1139
- Returns True given a token and the token that preceds it if it
1140
- seems clear that the token is beginning a sentence.
1141
- """
1142
- # If a token (i) is preceded by a sentece break that is
1143
- # not a potential ordinal number or initial, and (ii) is
1144
- # alphabetic, then it is a a sentence-starter.
1145
- return ( prev_tok.sentbreak and
1146
- not (prev_tok.is_number or prev_tok.is_initial) and
1147
- cur_tok.is_alpha )
1148
-
1149
- def _find_sent_starters(self):
1150
- """
1151
- Uses collocation heuristics for each candidate token to
1152
- determine if it frequently starts sentences.
1153
- """
1154
- for typ in self._sent_starter_fdist:
1155
- if not typ:
1156
- continue
1157
-
1158
- typ_at_break_count = self._sent_starter_fdist[typ]
1159
- typ_count = self._type_fdist[typ]+self._type_fdist[typ+'.']
1160
- if typ_count < typ_at_break_count:
1161
- # needed after freq_threshold
1162
- continue
1163
-
1164
- ll = self._col_log_likelihood(self._sentbreak_count, typ_count,
1165
- typ_at_break_count,
1166
- self._type_fdist.N())
1167
-
1168
- if (ll >= self.SENT_STARTER and
1169
- self._type_fdist.N()/self._sentbreak_count >
1170
- typ_count/typ_at_break_count):
1171
-
1172
- yield typ, ll
1173
-
1174
- def _get_sentbreak_count(self, tokens):
1175
- """
1176
- Returns the number of sentence breaks marked in a given set of
1177
- augmented tokens.
1178
- """
1179
- return sum(1 for aug_tok in tokens if aug_tok.sentbreak)
1180
-
1181
-
1182
- ######################################################################
1183
- #{ Punkt Sentence Tokenizer
1184
- ######################################################################
1185
-
1186
-
1187
- class PunktSentenceTokenizer(PunktBaseClass,TokenizerI):
1188
- """
1189
- A sentence tokenizer which uses an unsupervised algorithm to build
1190
- a model for abbreviation words, collocations, and words that start
1191
- sentences; and then uses that model to find sentence boundaries.
1192
- This approach has been shown to work well for many European
1193
- languages.
1194
- """
1195
- def __init__(self, train_text=None, verbose=False,
1196
- lang_vars=PunktLanguageVars(), token_cls=PunktToken):
1197
- """
1198
- train_text can either be the sole training text for this sentence
1199
- boundary detector, or can be a PunktParameters object.
1200
- """
1201
- PunktBaseClass.__init__(self, lang_vars=lang_vars,
1202
- token_cls=token_cls)
1203
-
1204
- if train_text:
1205
- self._params = self.train(train_text, verbose)
1206
-
1207
- def train(self, train_text, verbose=False):
1208
- """
1209
- Derives parameters from a given training text, or uses the parameters
1210
- given. Repeated calls to this method destroy previous parameters. For
1211
- incremental training, instantiate a separate PunktTrainer instance.
1212
- """
1213
- if not isinstance(train_text, string_types):
1214
- return train_text
1215
- return PunktTrainer(train_text, lang_vars=self._lang_vars,
1216
- token_cls=self._Token).get_params()
1217
-
1218
- #////////////////////////////////////////////////////////////
1219
- #{ Tokenization
1220
- #////////////////////////////////////////////////////////////
1221
-
1222
- def tokenize(self, text, realign_boundaries=True):
1223
- """
1224
- Given a text, returns a list of the sentences in that text.
1225
- """
1226
- return list(self.sentences_from_text(text, realign_boundaries))
1227
-
1228
- def debug_decisions(self, text):
1229
- """
1230
- Classifies candidate periods as sentence breaks, yielding a dict for
1231
- each that may be used to understand why the decision was made.
1232
-
1233
- See format_debug_decision() to help make this output readable.
1234
- """
1235
-
1236
- for match in self._lang_vars.period_context_re().finditer(text):
1237
- decision_text = match.group() + match.group('after_tok')
1238
- tokens = self._tokenize_words(decision_text)
1239
- tokens = list(self._annotate_first_pass(tokens))
1240
- while not tokens[0].period_final:
1241
- tokens.pop(0)
1242
- yield dict(period_index=match.end() - 1,
1243
- text=decision_text,
1244
- type1=tokens[0].type,
1245
- type2=tokens[1].type,
1246
- type1_in_abbrs=bool(tokens[0].abbr),
1247
- type1_is_initial=bool(tokens[0].is_initial),
1248
- type2_is_sent_starter=tokens[1].type_no_sentperiod in self._params.sent_starters,
1249
- type2_ortho_heuristic=self._ortho_heuristic(tokens[1]),
1250
- type2_ortho_contexts=set(self._params._debug_ortho_context(tokens[1].type_no_sentperiod)),
1251
- collocation=(tokens[0].type_no_sentperiod, tokens[1].type_no_sentperiod) in self._params.collocations,
1252
-
1253
- reason=self._second_pass_annotation(tokens[0], tokens[1]) or REASON_DEFAULT_DECISION,
1254
- break_decision=tokens[0].sentbreak,
1255
- )
1256
-
1257
- def span_tokenize(self, text, realign_boundaries=True):
1258
- """
1259
- Given a text, returns a list of the (start, end) spans of sentences
1260
- in the text.
1261
- """
1262
- slices = self._slices_from_text(text)
1263
- if realign_boundaries:
1264
- slices = self._realign_boundaries(text, slices)
1265
- return [(sl.start, sl.stop) for sl in slices]
1266
-
1267
- def sentences_from_text(self, text, realign_boundaries=True):
1268
- """
1269
- Given a text, generates the sentences in that text by only
1270
- testing candidate sentence breaks. If realign_boundaries is
1271
- True, includes in the sentence closing punctuation that
1272
- follows the period.
1273
- """
1274
- return [text[s:e] for s, e in self.span_tokenize(text, realign_boundaries)]
1275
-
1276
- def _slices_from_text(self, text):
1277
- last_break = 0
1278
- for match in self._lang_vars.period_context_re().finditer(text):
1279
- context = match.group() + match.group('after_tok')
1280
- if self.text_contains_sentbreak(context):
1281
- yield slice(last_break, match.end())
1282
- if match.group('next_tok'):
1283
- # next sentence starts after whitespace
1284
- last_break = match.start('next_tok')
1285
- else:
1286
- # next sentence starts at following punctuation
1287
- last_break = match.end()
1288
- yield slice(last_break, len(text))
1289
-
1290
- def _realign_boundaries(self, text, slices):
1291
- """
1292
- Attempts to realign punctuation that falls after the period but
1293
- should otherwise be included in the same sentence.
1294
-
1295
- For example: "(Sent1.) Sent2." will otherwise be split as::
1296
-
1297
- ["(Sent1.", ") Sent1."].
1298
-
1299
- This method will produce::
1300
-
1301
- ["(Sent1.)", "Sent2."].
1302
- """
1303
- realign = 0
1304
- for sl1, sl2 in _pair_iter(slices):
1305
- sl1 = slice(sl1.start + realign, sl1.stop)
1306
- if not sl2:
1307
- if text[sl1]:
1308
- yield sl1
1309
- continue
1310
-
1311
- m = self._lang_vars.re_boundary_realignment.match(text[sl2])
1312
- if m:
1313
- yield slice(sl1.start, sl2.start + len(m.group(0).rstrip()))
1314
- realign = m.end()
1315
- else:
1316
- realign = 0
1317
- if text[sl1]:
1318
- yield sl1
1319
-
1320
- def text_contains_sentbreak(self, text):
1321
- """
1322
- Returns True if the given text includes a sentence break.
1323
- """
1324
- found = False # used to ignore last token
1325
- for t in self._annotate_tokens(self._tokenize_words(text)):
1326
- if found:
1327
- return True
1328
- if t.sentbreak:
1329
- found = True
1330
- return False
1331
-
1332
- def sentences_from_text_legacy(self, text):
1333
- """
1334
- Given a text, generates the sentences in that text. Annotates all
1335
- tokens, rather than just those with possible sentence breaks. Should
1336
- produce the same results as ``sentences_from_text``.
1337
- """
1338
- tokens = self._annotate_tokens(self._tokenize_words(text))
1339
- return self._build_sentence_list(text, tokens)
1340
-
1341
- def sentences_from_tokens(self, tokens):
1342
- """
1343
- Given a sequence of tokens, generates lists of tokens, each list
1344
- corresponding to a sentence.
1345
- """
1346
- tokens = iter(self._annotate_tokens(self._Token(t) for t in tokens))
1347
- sentence = []
1348
- for aug_tok in tokens:
1349
- sentence.append(aug_tok.tok)
1350
- if aug_tok.sentbreak:
1351
- yield sentence
1352
- sentence = []
1353
- if sentence:
1354
- yield sentence
1355
-
1356
- def _annotate_tokens(self, tokens):
1357
- """
1358
- Given a set of tokens augmented with markers for line-start and
1359
- paragraph-start, returns an iterator through those tokens with full
1360
- annotation including predicted sentence breaks.
1361
- """
1362
- # Make a preliminary pass through the document, marking likely
1363
- # sentence breaks, abbreviations, and ellipsis tokens.
1364
- tokens = self._annotate_first_pass(tokens)
1365
-
1366
- # Make a second pass through the document, using token context
1367
- # information to change our preliminary decisions about where
1368
- # sentence breaks, abbreviations, and ellipsis occurs.
1369
- tokens = self._annotate_second_pass(tokens)
1370
-
1371
- ## [XX] TESTING
1372
- #tokens = list(tokens)
1373
- #self.dump(tokens)
1374
-
1375
- return tokens
1376
-
1377
- def _build_sentence_list(self, text, tokens):
1378
- """
1379
- Given the original text and the list of augmented word tokens,
1380
- construct and return a tokenized list of sentence strings.
1381
- """
1382
- # Most of the work here is making sure that we put the right
1383
- # pieces of whitespace back in all the right places.
1384
-
1385
- # Our position in the source text, used to keep track of which
1386
- # whitespace to add:
1387
- pos = 0
1388
-
1389
- # A regular expression that finds pieces of whitespace:
1390
- WS_REGEXP = re.compile(r'\s*')
1391
-
1392
- sentence = ''
1393
- for aug_tok in tokens:
1394
- tok = aug_tok.tok
1395
-
1396
- # Find the whitespace before this token, and update pos.
1397
- ws = WS_REGEXP.match(text, pos).group()
1398
- pos += len(ws)
1399
-
1400
- # Some of the rules used by the punkt word tokenizer
1401
- # strip whitespace out of the text, resulting in tokens
1402
- # that contain whitespace in the source text. If our
1403
- # token doesn't match, see if adding whitespace helps.
1404
- # If so, then use the version with whitespace.
1405
- if text[pos:pos+len(tok)] != tok:
1406
- pat = '\s*'.join(re.escape(c) for c in tok)
1407
- m = re.compile(pat).match(text,pos)
1408
- if m: tok = m.group()
1409
-
1410
- # Move our position pointer to the end of the token.
1411
- assert text[pos:pos+len(tok)] == tok
1412
- pos += len(tok)
1413
-
1414
- # Add this token. If it's not at the beginning of the
1415
- # sentence, then include any whitespace that separated it
1416
- # from the previous token.
1417
- if sentence:
1418
- sentence += ws
1419
- sentence += tok
1420
-
1421
- # If we're at a sentence break, then start a new sentence.
1422
- if aug_tok.sentbreak:
1423
- yield sentence
1424
- sentence = ''
1425
-
1426
- # If the last sentence is emtpy, discard it.
1427
- if sentence:
1428
- yield sentence
1429
-
1430
- # [XX] TESTING
1431
- def dump(self, tokens):
1432
- print('writing to /tmp/punkt.new...')
1433
- with open('/tmp/punkt.new', 'w') as outfile:
1434
- for aug_tok in tokens:
1435
- if aug_tok.parastart:
1436
- outfile.write('\n\n')
1437
- elif aug_tok.linestart:
1438
- outfile.write('\n')
1439
- else:
1440
- outfile.write(' ')
1441
-
1442
- outfile.write(str(aug_tok))
1443
-
1444
- #////////////////////////////////////////////////////////////
1445
- #{ Customization Variables
1446
- #////////////////////////////////////////////////////////////
1447
-
1448
- PUNCTUATION = tuple(';:,.!?')
1449
-
1450
- #////////////////////////////////////////////////////////////
1451
- #{ Annotation Procedures
1452
- #////////////////////////////////////////////////////////////
1453
-
1454
- def _annotate_second_pass(self, tokens):
1455
- """
1456
- Performs a token-based classification (section 4) over the given
1457
- tokens, making use of the orthographic heuristic (4.1.1), collocation
1458
- heuristic (4.1.2) and frequent sentence starter heuristic (4.1.3).
1459
- """
1460
- for t1, t2 in _pair_iter(tokens):
1461
- self._second_pass_annotation(t1, t2)
1462
- yield t1
1463
-
1464
- def _second_pass_annotation(self, aug_tok1, aug_tok2):
1465
- """
1466
- Performs token-based classification over a pair of contiguous tokens
1467
- updating the first.
1468
- """
1469
- # Is it the last token? We can't do anything then.
1470
- if not aug_tok2:
1471
- return
1472
-
1473
- tok = aug_tok1.tok
1474
- if not aug_tok1.period_final:
1475
- # We only care about words ending in periods.
1476
- return
1477
-
1478
- typ = aug_tok1.type_no_period
1479
- next_tok = aug_tok2.tok
1480
- next_typ = aug_tok2.type_no_sentperiod
1481
- tok_is_initial = aug_tok1.is_initial
1482
-
1483
- # [4.1.2. Collocation Heuristic] If there's a
1484
- # collocation between the word before and after the
1485
- # period, then label tok as an abbreviation and NOT
1486
- # a sentence break. Note that collocations with
1487
- # frequent sentence starters as their second word are
1488
- # excluded in training.
1489
- if (typ, next_typ) in self._params.collocations:
1490
- aug_tok1.sentbreak = False
1491
- aug_tok1.abbr = True
1492
- return REASON_KNOWN_COLLOCATION
1493
-
1494
- # [4.2. Token-Based Reclassification of Abbreviations] If
1495
- # the token is an abbreviation or an ellipsis, then decide
1496
- # whether we should *also* classify it as a sentbreak.
1497
- if ( (aug_tok1.abbr or aug_tok1.ellipsis) and
1498
- (not tok_is_initial) ):
1499
- # [4.1.1. Orthographic Heuristic] Check if there's
1500
- # orthogrpahic evidence about whether the next word
1501
- # starts a sentence or not.
1502
- is_sent_starter = self._ortho_heuristic(aug_tok2)
1503
- if is_sent_starter == True:
1504
- aug_tok1.sentbreak = True
1505
- return REASON_ABBR_WITH_ORTHOGRAPHIC_HEURISTIC
1506
-
1507
- # [4.1.3. Frequent Sentence Starter Heruistic] If the
1508
- # next word is capitalized, and is a member of the
1509
- # frequent-sentence-starters list, then label tok as a
1510
- # sentence break.
1511
- if ( aug_tok2.first_upper and
1512
- next_typ in self._params.sent_starters):
1513
- aug_tok1.sentbreak = True
1514
- return REASON_ABBR_WITH_SENTENCE_STARTER
1515
-
1516
- # [4.3. Token-Based Detection of Initials and Ordinals]
1517
- # Check if any initials or ordinals tokens that are marked
1518
- # as sentbreaks should be reclassified as abbreviations.
1519
- if tok_is_initial or typ == '##number##':
1520
-
1521
- # [4.1.1. Orthographic Heuristic] Check if there's
1522
- # orthogrpahic evidence about whether the next word
1523
- # starts a sentence or not.
1524
- is_sent_starter = self._ortho_heuristic(aug_tok2)
1525
-
1526
- if is_sent_starter == False:
1527
- aug_tok1.sentbreak = False
1528
- aug_tok1.abbr = True
1529
- if tok_is_initial:
1530
- return REASON_INITIAL_WITH_ORTHOGRAPHIC_HEURISTIC
1531
- else:
1532
- return REASON_NUMBER_WITH_ORTHOGRAPHIC_HEURISTIC
1533
-
1534
- # Special heuristic for initials: if orthogrpahic
1535
- # heuristc is unknown, and next word is always
1536
- # capitalized, then mark as abbrev (eg: J. Bach).
1537
- if ( is_sent_starter == 'unknown' and tok_is_initial and
1538
- aug_tok2.first_upper and
1539
- not (self._params.ortho_context[next_typ] & _ORTHO_LC) ):
1540
- aug_tok1.sentbreak = False
1541
- aug_tok1.abbr = True
1542
- return REASON_INITIAL_WITH_SPECIAL_ORTHOGRAPHIC_HEURISTIC
1543
-
1544
- return
1545
-
1546
- def _ortho_heuristic(self, aug_tok):
1547
- """
1548
- Decide whether the given token is the first token in a sentence.
1549
- """
1550
- # Sentences don't start with punctuation marks:
1551
- if aug_tok.tok in self.PUNCTUATION:
1552
- return False
1553
-
1554
- ortho_context = self._params.ortho_context[aug_tok.type_no_sentperiod]
1555
-
1556
- # If the word is capitalized, occurs at least once with a
1557
- # lower case first letter, and never occurs with an upper case
1558
- # first letter sentence-internally, then it's a sentence starter.
1559
- if ( aug_tok.first_upper and
1560
- (ortho_context & _ORTHO_LC) and
1561
- not (ortho_context & _ORTHO_MID_UC) ):
1562
- return True
1563
-
1564
- # If the word is lower case, and either (a) we've seen it used
1565
- # with upper case, or (b) we've never seen it used
1566
- # sentence-initially with lower case, then it's not a sentence
1567
- # starter.
1568
- if ( aug_tok.first_lower and
1569
- ((ortho_context & _ORTHO_UC) or
1570
- not (ortho_context & _ORTHO_BEG_LC)) ):
1571
- return False
1572
-
1573
- # Otherwise, we're not sure.
1574
- return 'unknown'
1575
-
1576
-
1577
- DEBUG_DECISION_FMT = '''Text: %(text)r (at offset %(period_index)d)
1578
- Sentence break? %(break_decision)s (%(reason)s)
1579
- Collocation? %(collocation)s
1580
- %(type1)r:
1581
- known abbreviation: %(type1_in_abbrs)s
1582
- is initial: %(type1_is_initial)s
1583
- %(type2)r:
1584
- known sentence starter: %(type2_is_sent_starter)s
1585
- orthographic heuristic suggests is a sentence starter? %(type2_ortho_heuristic)s
1586
- orthographic contexts in training: %(type2_ortho_contexts)s
1587
- '''
1588
- def format_debug_decision(d):
1589
- return DEBUG_DECISION_FMT % d
1590
-
1591
- def demo(text, tok_cls=PunktSentenceTokenizer, train_cls=PunktTrainer):
1592
- """Builds a punkt model and applies it to the same text"""
1593
- cleanup = lambda s: re.compile(r'(?:\r|^\s+)', re.MULTILINE).sub('', s).replace('\n', ' ')
1594
- trainer = train_cls()
1595
- trainer.INCLUDE_ALL_COLLOCS = True
1596
- trainer.train(text)
1597
- sbd = tok_cls(trainer.get_params())
1598
- for l in sbd.sentences_from_text(text):
1599
- print(cleanup(l))
1600
-
1601
-
1
+ # Natural Language Toolkit: Punkt sentence tokenizer
2
+ #
3
+ # Copyright (C) 2001-2018 NLTK Project
4
+ # Algorithm: Kiss & Strunk (2006)
5
+ # Author: Willy <willy@csse.unimelb.edu.au> (original Python port)
6
+ # Steven Bird <stevenbird1@gmail.com> (additions)
7
+ # Edward Loper <edloper@gmail.com> (rewrite)
8
+ # Joel Nothman <jnothman@student.usyd.edu.au> (almost rewrite)
9
+ # Arthur Darcet <arthur@darcet.fr> (fixes)
10
+ # URL: <http://nltk.org/>
11
+ # For license information, see LICENSE.TXT
12
+
13
+ r"""
14
+ Punkt Sentence Tokenizer
15
+
16
+ This tokenizer divides a text into a list of sentences
17
+ by using an unsupervised algorithm to build a model for abbreviation
18
+ words, collocations, and words that start sentences. It must be
19
+ trained on a large collection of plaintext in the target language
20
+ before it can be used.
21
+
22
+ The NLTK data package includes a pre-trained Punkt tokenizer for
23
+ English.
24
+
25
+ >>> import nltk.data
26
+ >>> text = '''
27
+ ... Punkt knows that the periods in Mr. Smith and Johann S. Bach
28
+ ... do not mark sentence boundaries. And sometimes sentences
29
+ ... can start with non-capitalized words. i is a good variable
30
+ ... name.
31
+ ... '''
32
+ >>> sent_detector = nltk.data.load('tokenizers/punkt/english.pickle')
33
+ >>> print('\n-----\n'.join(sent_detector.tokenize(text.strip())))
34
+ Punkt knows that the periods in Mr. Smith and Johann S. Bach
35
+ do not mark sentence boundaries.
36
+ -----
37
+ And sometimes sentences
38
+ can start with non-capitalized words.
39
+ -----
40
+ i is a good variable
41
+ name.
42
+
43
+ (Note that whitespace from the original text, including newlines, is
44
+ retained in the output.)
45
+
46
+ Punctuation following sentences is also included by default
47
+ (from NLTK 3.0 onwards). It can be excluded with the realign_boundaries
48
+ flag.
49
+
50
+ >>> text = '''
51
+ ... (How does it deal with this parenthesis?) "It should be part of the
52
+ ... previous sentence." "(And the same with this one.)" ('And this one!')
53
+ ... "('(And (this)) '?)" [(and this. )]
54
+ ... '''
55
+ >>> print('\n-----\n'.join(
56
+ ... sent_detector.tokenize(text.strip())))
57
+ (How does it deal with this parenthesis?)
58
+ -----
59
+ "It should be part of the
60
+ previous sentence."
61
+ -----
62
+ "(And the same with this one.)"
63
+ -----
64
+ ('And this one!')
65
+ -----
66
+ "('(And (this)) '?)"
67
+ -----
68
+ [(and this. )]
69
+ >>> print('\n-----\n'.join(
70
+ ... sent_detector.tokenize(text.strip(), realign_boundaries=False)))
71
+ (How does it deal with this parenthesis?
72
+ -----
73
+ ) "It should be part of the
74
+ previous sentence.
75
+ -----
76
+ " "(And the same with this one.
77
+ -----
78
+ )" ('And this one!
79
+ -----
80
+ ')
81
+ "('(And (this)) '?
82
+ -----
83
+ )" [(and this.
84
+ -----
85
+ )]
86
+
87
+ However, Punkt is designed to learn parameters (a list of abbreviations, etc.)
88
+ unsupervised from a corpus similar to the target domain. The pre-packaged models
89
+ may therefore be unsuitable: use ``PunktSentenceTokenizer(text)`` to learn
90
+ parameters from the given text.
91
+
92
+ :class:`.PunktTrainer` learns parameters such as a list of abbreviations
93
+ (without supervision) from portions of text. Using a ``PunktTrainer`` directly
94
+ allows for incremental training and modification of the hyper-parameters used
95
+ to decide what is considered an abbreviation, etc.
96
+
97
+ The algorithm for this tokenizer is described in::
98
+
99
+ Kiss, Tibor and Strunk, Jan (2006): Unsupervised Multilingual Sentence
100
+ Boundary Detection. Computational Linguistics 32: 485-525.
101
+ """
102
+ from __future__ import print_function, unicode_literals, division
103
+
104
+ # TODO: Make orthographic heuristic less susceptible to overtraining
105
+ # TODO: Frequent sentence starters optionally exclude always-capitalised words
106
+ # FIXME: Problem with ending string with e.g. '!!!' -> '!! !'
107
+
108
+ import re
109
+ import math
110
+ from collections import defaultdict
111
+
112
+ from six import string_types
113
+
114
+ from nltk.compat import unicode_repr, python_2_unicode_compatible
115
+ from nltk.probability import FreqDist
116
+ from nltk.tokenize.api import TokenizerI
117
+
118
+ ######################################################################
119
+ # { Orthographic Context Constants
120
+ ######################################################################
121
+ # The following constants are used to describe the orthographic
122
+ # contexts in which a word can occur. BEG=beginning, MID=middle,
123
+ # UNK=unknown, UC=uppercase, LC=lowercase, NC=no case.
124
+
125
+ _ORTHO_BEG_UC = 1 << 1
126
+ """Orthographic context: beginning of a sentence with upper case."""
127
+
128
+ _ORTHO_MID_UC = 1 << 2
129
+ """Orthographic context: middle of a sentence with upper case."""
130
+
131
+ _ORTHO_UNK_UC = 1 << 3
132
+ """Orthographic context: unknown position in a sentence with upper case."""
133
+
134
+ _ORTHO_BEG_LC = 1 << 4
135
+ """Orthographic context: beginning of a sentence with lower case."""
136
+
137
+ _ORTHO_MID_LC = 1 << 5
138
+ """Orthographic context: middle of a sentence with lower case."""
139
+
140
+ _ORTHO_UNK_LC = 1 << 6
141
+ """Orthographic context: unknown position in a sentence with lower case."""
142
+
143
+ _ORTHO_UC = _ORTHO_BEG_UC + _ORTHO_MID_UC + _ORTHO_UNK_UC
144
+ """Orthographic context: occurs with upper case."""
145
+
146
+ _ORTHO_LC = _ORTHO_BEG_LC + _ORTHO_MID_LC + _ORTHO_UNK_LC
147
+ """Orthographic context: occurs with lower case."""
148
+
149
+ _ORTHO_MAP = {
150
+ ('initial', 'upper'): _ORTHO_BEG_UC,
151
+ ('internal', 'upper'): _ORTHO_MID_UC,
152
+ ('unknown', 'upper'): _ORTHO_UNK_UC,
153
+ ('initial', 'lower'): _ORTHO_BEG_LC,
154
+ ('internal', 'lower'): _ORTHO_MID_LC,
155
+ ('unknown', 'lower'): _ORTHO_UNK_LC,
156
+ }
157
+ """A map from context position and first-letter case to the
158
+ appropriate orthographic context flag."""
159
+
160
+ # } (end orthographic context constants)
161
+ ######################################################################
162
+
163
+ ######################################################################
164
+ # { Decision reasons for debugging
165
+ ######################################################################
166
+
167
+ REASON_DEFAULT_DECISION = 'default decision'
168
+ REASON_KNOWN_COLLOCATION = 'known collocation (both words)'
169
+ REASON_ABBR_WITH_ORTHOGRAPHIC_HEURISTIC = 'abbreviation + orthographic heuristic'
170
+ REASON_ABBR_WITH_SENTENCE_STARTER = 'abbreviation + frequent sentence starter'
171
+ REASON_INITIAL_WITH_ORTHOGRAPHIC_HEURISTIC = 'initial + orthographic heuristic'
172
+ REASON_NUMBER_WITH_ORTHOGRAPHIC_HEURISTIC = 'initial + orthographic heuristic'
173
+ REASON_INITIAL_WITH_SPECIAL_ORTHOGRAPHIC_HEURISTIC = 'initial + special orthographic heuristic'
174
+
175
+
176
+ # } (end decision reasons for debugging)
177
+ ######################################################################
178
+
179
+ ######################################################################
180
+ # { Language-dependent variables
181
+ ######################################################################
182
+
183
+ class PunktLanguageVars(object):
184
+ """
185
+ Stores variables, mostly regular expressions, which may be
186
+ language-dependent for correct application of the algorithm.
187
+ An extension of this class may modify its properties to suit
188
+ a language other than English; an instance can then be passed
189
+ as an argument to PunktSentenceTokenizer and PunktTrainer
190
+ constructors.
191
+ """
192
+
193
+ __slots__ = ('_re_period_context', '_re_word_tokenizer')
194
+
195
+ def __getstate__(self):
196
+ # All modifications to the class are performed by inheritance.
197
+ # Non-default parameters to be pickled must be defined in the inherited
198
+ # class.
199
+ return 1
200
+
201
+ def __setstate__(self, state):
202
+ return 1
203
+
204
+ sent_end_chars = ('.', '?', '!')
205
+ """Characters which are candidates for sentence boundaries"""
206
+
207
+ @property
208
+ def _re_sent_end_chars(self):
209
+ return '[%s]' % re.escape(''.join(self.sent_end_chars))
210
+
211
+ internal_punctuation = ',:;' # might want to extend this..
212
+ """sentence internal punctuation, which indicates an abbreviation if
213
+ preceded by a period-final token."""
214
+
215
+ re_boundary_realignment = re.compile(r'["\')\]}]+?(?:\s+|(?=--)|$)',
216
+ re.MULTILINE)
217
+ """Used to realign punctuation that should be included in a sentence
218
+ although it follows the period (or ?, !)."""
219
+
220
+ _re_word_start = r"[^\(\"\`{\[:;&\#\*@\)}\]\-,]"
221
+ """Excludes some characters from starting word tokens"""
222
+
223
+ _re_non_word_chars = r"(?:[?!)\";}\]\*:@\'\({\[])"
224
+ """Characters that cannot appear within words"""
225
+
226
+ _re_multi_char_punct = r"(?:\-{2,}|\.{2,}|(?:\.\s){2,}\.)"
227
+ """Hyphen and ellipsis are multi-character punctuation"""
228
+
229
+ _word_tokenize_fmt = r'''(
230
+ %(MultiChar)s
231
+ |
232
+ (?=%(WordStart)s)\S+? # Accept word characters until end is found
233
+ (?= # Sequences marking a word's end
234
+ \s| # White-space
235
+ $| # End-of-string
236
+ %(NonWord)s|%(MultiChar)s| # Punctuation
237
+ ,(?=$|\s|%(NonWord)s|%(MultiChar)s) # Comma if at end of word
238
+ )
239
+ |
240
+ \S
241
+ )'''
242
+ """Format of a regular expression to split punctuation from words,
243
+ excluding period."""
244
+
245
+ def _word_tokenizer_re(self):
246
+ """Compiles and returns a regular expression for word tokenization"""
247
+ try:
248
+ return self._re_word_tokenizer
249
+ except AttributeError:
250
+ self._re_word_tokenizer = re.compile(
251
+ self._word_tokenize_fmt %
252
+ {
253
+ 'NonWord': self._re_non_word_chars,
254
+ 'MultiChar': self._re_multi_char_punct,
255
+ 'WordStart': self._re_word_start,
256
+ },
257
+ re.UNICODE | re.VERBOSE
258
+ )
259
+ return self._re_word_tokenizer
260
+
261
+ def word_tokenize(self, s):
262
+ """Tokenize a string to split off punctuation other than periods"""
263
+ return self._word_tokenizer_re().findall(s)
264
+
265
+ _period_context_fmt = r"""
266
+ \S* # some word material
267
+ %(SentEndChars)s # a potential sentence ending
268
+ (?=(?P<after_tok>
269
+ %(NonWord)s # either other punctuation
270
+ |
271
+ \s+(?P<next_tok>\S+) # or whitespace and some other token
272
+ ))"""
273
+ """Format of a regular expression to find contexts including possible
274
+ sentence boundaries. Matches token which the possible sentence boundary
275
+ ends, and matches the following token within a lookahead expression."""
276
+
277
+ def period_context_re(self):
278
+ """Compiles and returns a regular expression to find contexts
279
+ including possible sentence boundaries."""
280
+ try:
281
+ return self._re_period_context
282
+ except:
283
+ self._re_period_context = re.compile(
284
+ self._period_context_fmt %
285
+ {
286
+ 'NonWord': self._re_non_word_chars,
287
+ 'SentEndChars': self._re_sent_end_chars,
288
+ },
289
+ re.UNICODE | re.VERBOSE)
290
+ return self._re_period_context
291
+
292
+
293
+ _re_non_punct = re.compile(r'[^\W\d]', re.UNICODE)
294
+ """Matches token types that are not merely punctuation. (Types for
295
+ numeric tokens are changed to ##number## and hence contain alpha.)"""
296
+
297
+
298
+ # }
299
+ ######################################################################
300
+
301
+
302
+ # ////////////////////////////////////////////////////////////
303
+ # { Helper Functions
304
+ # ////////////////////////////////////////////////////////////
305
+
306
+ def _pair_iter(it):
307
+ """
308
+ Yields pairs of tokens from the given iterator such that each input
309
+ token will appear as the first element in a yielded tuple. The last
310
+ pair will have None as its second element.
311
+ """
312
+ it = iter(it)
313
+ prev = next(it)
314
+ for el in it:
315
+ yield (prev, el)
316
+ prev = el
317
+ yield (prev, None)
318
+
319
+
320
+ ######################################################################
321
+ # { Punkt Parameters
322
+ ######################################################################
323
+
324
+ class PunktParameters(object):
325
+ """Stores data used to perform sentence boundary detection with Punkt."""
326
+
327
+ def __init__(self):
328
+ self.abbrev_types = set()
329
+ """A set of word types for known abbreviations."""
330
+
331
+ self.collocations = set()
332
+ """A set of word type tuples for known common collocations
333
+ where the first word ends in a period. E.g., ('S.', 'Bach')
334
+ is a common collocation in a text that discusses 'Johann
335
+ S. Bach'. These count as negative evidence for sentence
336
+ boundaries."""
337
+
338
+ self.sent_starters = set()
339
+ """A set of word types for words that often appear at the
340
+ beginning of sentences."""
341
+
342
+ self.ortho_context = defaultdict(int)
343
+ """A dictionary mapping word types to the set of orthographic
344
+ contexts that word type appears in. Contexts are represented
345
+ by adding orthographic context flags: ..."""
346
+
347
+ def clear_abbrevs(self):
348
+ self.abbrev_types = set()
349
+
350
+ def clear_collocations(self):
351
+ self.collocations = set()
352
+
353
+ def clear_sent_starters(self):
354
+ self.sent_starters = set()
355
+
356
+ def clear_ortho_context(self):
357
+ self.ortho_context = defaultdict(int)
358
+
359
+ def add_ortho_context(self, typ, flag):
360
+ self.ortho_context[typ] |= flag
361
+
362
+ def _debug_ortho_context(self, typ):
363
+ c = self.ortho_context[typ]
364
+ if c & _ORTHO_BEG_UC:
365
+ yield 'BEG-UC'
366
+ if c & _ORTHO_MID_UC:
367
+ yield 'MID-UC'
368
+ if c & _ORTHO_UNK_UC:
369
+ yield 'UNK-UC'
370
+ if c & _ORTHO_BEG_LC:
371
+ yield 'BEG-LC'
372
+ if c & _ORTHO_MID_LC:
373
+ yield 'MID-LC'
374
+ if c & _ORTHO_UNK_LC:
375
+ yield 'UNK-LC'
376
+
377
+
378
+ ######################################################################
379
+ # { PunktToken
380
+ ######################################################################
381
+
382
+ @python_2_unicode_compatible
383
+ class PunktToken(object):
384
+ """Stores a token of text with annotations produced during
385
+ sentence boundary detection."""
386
+
387
+ _properties = [
388
+ 'parastart', 'linestart',
389
+ 'sentbreak', 'abbr', 'ellipsis'
390
+ ]
391
+ __slots__ = ['tok', 'type', 'period_final'] + _properties
392
+
393
+ def __init__(self, tok, **params):
394
+ self.tok = tok
395
+ self.type = self._get_type(tok)
396
+ self.period_final = tok.endswith('.')
397
+
398
+ for p in self._properties:
399
+ setattr(self, p, None)
400
+ for k in params:
401
+ setattr(self, k, params[k])
402
+
403
+ # ////////////////////////////////////////////////////////////
404
+ # { Regular expressions for properties
405
+ # ////////////////////////////////////////////////////////////
406
+ # Note: [A-Za-z] is approximated by [^\W\d] in the general case.
407
+ _RE_ELLIPSIS = re.compile(r'\.\.+$')
408
+ _RE_NUMERIC = re.compile(r'^-?[\.,]?\d[\d,\.-]*\.?$')
409
+ _RE_INITIAL = re.compile(r'[^\W\d]\.$', re.UNICODE)
410
+ _RE_ALPHA = re.compile(r'[^\W\d]+$', re.UNICODE)
411
+
412
+ # ////////////////////////////////////////////////////////////
413
+ # { Derived properties
414
+ # ////////////////////////////////////////////////////////////
415
+
416
+ def _get_type(self, tok):
417
+ """Returns a case-normalized representation of the token."""
418
+ return self._RE_NUMERIC.sub('##number##', tok.lower())
419
+
420
+ @property
421
+ def type_no_period(self):
422
+ """
423
+ The type with its final period removed if it has one.
424
+ """
425
+ if len(self.type) > 1 and self.type[-1] == '.':
426
+ return self.type[:-1]
427
+ return self.type
428
+
429
+ @property
430
+ def type_no_sentperiod(self):
431
+ """
432
+ The type with its final period removed if it is marked as a
433
+ sentence break.
434
+ """
435
+ if self.sentbreak:
436
+ return self.type_no_period
437
+ return self.type
438
+
439
+ @property
440
+ def first_upper(self):
441
+ """True if the token's first character is uppercase."""
442
+ return self.tok[0].isupper()
443
+
444
+ @property
445
+ def first_lower(self):
446
+ """True if the token's first character is lowercase."""
447
+ return self.tok[0].islower()
448
+
449
+ @property
450
+ def first_case(self):
451
+ if self.first_lower:
452
+ return 'lower'
453
+ elif self.first_upper:
454
+ return 'upper'
455
+ return 'none'
456
+
457
+ @property
458
+ def is_ellipsis(self):
459
+ """True if the token text is that of an ellipsis."""
460
+ return self._RE_ELLIPSIS.match(self.tok)
461
+
462
+ @property
463
+ def is_number(self):
464
+ """True if the token text is that of a number."""
465
+ return self.type.startswith('##number##')
466
+
467
+ @property
468
+ def is_initial(self):
469
+ """True if the token text is that of an initial."""
470
+ return self._RE_INITIAL.match(self.tok)
471
+
472
+ @property
473
+ def is_alpha(self):
474
+ """True if the token text is all alphabetic."""
475
+ return self._RE_ALPHA.match(self.tok)
476
+
477
+ @property
478
+ def is_non_punct(self):
479
+ """True if the token is either a number or is alphabetic."""
480
+ return _re_non_punct.search(self.type)
481
+
482
+ # ////////////////////////////////////////////////////////////
483
+ # { String representation
484
+ # ////////////////////////////////////////////////////////////
485
+
486
+ def __repr__(self):
487
+ """
488
+ A string representation of the token that can reproduce it
489
+ with eval(), which lists all the token's non-default
490
+ annotations.
491
+ """
492
+ typestr = (' type=%s,' % unicode_repr(self.type)
493
+ if self.type != self.tok else '')
494
+
495
+ propvals = ', '.join(
496
+ '%s=%s' % (p, unicode_repr(getattr(self, p)))
497
+ for p in self._properties
498
+ if getattr(self, p)
499
+ )
500
+
501
+ return '%s(%s,%s %s)' % (self.__class__.__name__,
502
+ unicode_repr(self.tok), typestr, propvals)
503
+
504
+ def __str__(self):
505
+ """
506
+ A string representation akin to that used by Kiss and Strunk.
507
+ """
508
+ res = self.tok
509
+ if self.abbr:
510
+ res += '<A>'
511
+ if self.ellipsis:
512
+ res += '<E>'
513
+ if self.sentbreak:
514
+ res += '<S>'
515
+ return res
516
+
517
+
518
+ ######################################################################
519
+ # { Punkt base class
520
+ ######################################################################
521
+
522
+ class PunktBaseClass(object):
523
+ """
524
+ Includes common components of PunktTrainer and PunktSentenceTokenizer.
525
+ """
526
+
527
+ def __init__(self, lang_vars=PunktLanguageVars(), token_cls=PunktToken,
528
+ params=None):
529
+ if params is None:
530
+ params = PunktParameters()
531
+ self._params = params
532
+ self._lang_vars = lang_vars
533
+ self._Token = token_cls
534
+ """The collection of parameters that determines the behavior
535
+ of the punkt tokenizer."""
536
+
537
+ # ////////////////////////////////////////////////////////////
538
+ # { Word tokenization
539
+ # ////////////////////////////////////////////////////////////
540
+
541
+ def _tokenize_words(self, plaintext):
542
+ """
543
+ Divide the given text into tokens, using the punkt word
544
+ segmentation regular expression, and generate the resulting list
545
+ of tokens augmented as three-tuples with two boolean values for whether
546
+ the given token occurs at the start of a paragraph or a new line,
547
+ respectively.
548
+ """
549
+ parastart = False
550
+ for line in plaintext.split('\n'):
551
+ if line.strip():
552
+ line_toks = iter(self._lang_vars.word_tokenize(line))
553
+
554
+ yield self._Token(next(line_toks),
555
+ parastart=parastart, linestart=True)
556
+ parastart = False
557
+
558
+ for t in line_toks:
559
+ yield self._Token(t)
560
+ else:
561
+ parastart = True
562
+
563
+ # ////////////////////////////////////////////////////////////
564
+ # { Annotation Procedures
565
+ # ////////////////////////////////////////////////////////////
566
+
567
+ def _annotate_first_pass(self, tokens):
568
+ """
569
+ Perform the first pass of annotation, which makes decisions
570
+ based purely based on the word type of each word:
571
+
572
+ - '?', '!', and '.' are marked as sentence breaks.
573
+ - sequences of two or more periods are marked as ellipsis.
574
+ - any word ending in '.' that's a known abbreviation is
575
+ marked as an abbreviation.
576
+ - any other word ending in '.' is marked as a sentence break.
577
+
578
+ Return these annotations as a tuple of three sets:
579
+
580
+ - sentbreak_toks: The indices of all sentence breaks.
581
+ - abbrev_toks: The indices of all abbreviations.
582
+ - ellipsis_toks: The indices of all ellipsis marks.
583
+ """
584
+ for aug_tok in tokens:
585
+ self._first_pass_annotation(aug_tok)
586
+ yield aug_tok
587
+
588
+ def _first_pass_annotation(self, aug_tok):
589
+ """
590
+ Performs type-based annotation on a single token.
591
+ """
592
+
593
+ tok = aug_tok.tok
594
+
595
+ if tok in self._lang_vars.sent_end_chars:
596
+ aug_tok.sentbreak = True
597
+ elif aug_tok.is_ellipsis:
598
+ aug_tok.ellipsis = True
599
+ elif aug_tok.period_final and not tok.endswith('..'):
600
+ if (tok[:-1].lower() in self._params.abbrev_types or
601
+ tok[:-1].lower().split('-')[-1] in self._params.abbrev_types):
602
+
603
+ aug_tok.abbr = True
604
+ else:
605
+ aug_tok.sentbreak = True
606
+
607
+ return
608
+
609
+
610
+ ######################################################################
611
+ # { Punkt Trainer
612
+ ######################################################################
613
+
614
+
615
+ class PunktTrainer(PunktBaseClass):
616
+ """Learns parameters used in Punkt sentence boundary detection."""
617
+
618
+ def __init__(self, train_text=None, verbose=False,
619
+ lang_vars=PunktLanguageVars(), token_cls=PunktToken):
620
+
621
+ PunktBaseClass.__init__(self, lang_vars=lang_vars,
622
+ token_cls=token_cls)
623
+
624
+ self._type_fdist = FreqDist()
625
+ """A frequency distribution giving the frequency of each
626
+ case-normalized token type in the training data."""
627
+
628
+ self._num_period_toks = 0
629
+ """The number of words ending in period in the training data."""
630
+
631
+ self._collocation_fdist = FreqDist()
632
+ """A frequency distribution giving the frequency of all
633
+ bigrams in the training data where the first word ends in a
634
+ period. Bigrams are encoded as tuples of word types.
635
+ Especially common collocations are extracted from this
636
+ frequency distribution, and stored in
637
+ ``_params``.``collocations <PunktParameters.collocations>``."""
638
+
639
+ self._sent_starter_fdist = FreqDist()
640
+ """A frequency distribution giving the frequency of all words
641
+ that occur at the training data at the beginning of a sentence
642
+ (after the first pass of annotation). Especially common
643
+ sentence starters are extracted from this frequency
644
+ distribution, and stored in ``_params.sent_starters``.
645
+ """
646
+
647
+ self._sentbreak_count = 0
648
+ """The total number of sentence breaks identified in training, used for
649
+ calculating the frequent sentence starter heuristic."""
650
+
651
+ self._finalized = True
652
+ """A flag as to whether the training has been finalized by finding
653
+ collocations and sentence starters, or whether finalize_training()
654
+ still needs to be called."""
655
+
656
+ if train_text:
657
+ self.train(train_text, verbose, finalize=True)
658
+
659
+ def get_params(self):
660
+ """
661
+ Calculates and returns parameters for sentence boundary detection as
662
+ derived from training."""
663
+ if not self._finalized:
664
+ self.finalize_training()
665
+ return self._params
666
+
667
+ # ////////////////////////////////////////////////////////////
668
+ # { Customization Variables
669
+ # ////////////////////////////////////////////////////////////
670
+
671
+ ABBREV = 0.3
672
+ """cut-off value whether a 'token' is an abbreviation"""
673
+
674
+ IGNORE_ABBREV_PENALTY = False
675
+ """allows the disabling of the abbreviation penalty heuristic, which
676
+ exponentially disadvantages words that are found at times without a
677
+ final period."""
678
+
679
+ ABBREV_BACKOFF = 5
680
+ """upper cut-off for Mikheev's(2002) abbreviation detection algorithm"""
681
+
682
+ COLLOCATION = 7.88
683
+ """minimal log-likelihood value that two tokens need to be considered
684
+ as a collocation"""
685
+
686
+ SENT_STARTER = 30
687
+ """minimal log-likelihood value that a token requires to be considered
688
+ as a frequent sentence starter"""
689
+
690
+ INCLUDE_ALL_COLLOCS = False
691
+ """this includes as potential collocations all word pairs where the first
692
+ word ends in a period. It may be useful in corpora where there is a lot
693
+ of variation that makes abbreviations like Mr difficult to identify."""
694
+
695
+ INCLUDE_ABBREV_COLLOCS = False
696
+ """this includes as potential collocations all word pairs where the first
697
+ word is an abbreviation. Such collocations override the orthographic
698
+ heuristic, but not the sentence starter heuristic. This is overridden by
699
+ INCLUDE_ALL_COLLOCS, and if both are false, only collocations with initials
700
+ and ordinals are considered."""
701
+ """"""
702
+
703
+ MIN_COLLOC_FREQ = 1
704
+ """this sets a minimum bound on the number of times a bigram needs to
705
+ appear before it can be considered a collocation, in addition to log
706
+ likelihood statistics. This is useful when INCLUDE_ALL_COLLOCS is True."""
707
+
708
+ # ////////////////////////////////////////////////////////////
709
+ # { Training..
710
+ # ////////////////////////////////////////////////////////////
711
+
712
+ def train(self, text, verbose=False, finalize=True):
713
+ """
714
+ Collects training data from a given text. If finalize is True, it
715
+ will determine all the parameters for sentence boundary detection. If
716
+ not, this will be delayed until get_params() or finalize_training() is
717
+ called. If verbose is True, abbreviations found will be listed.
718
+ """
719
+ # Break the text into tokens; record which token indices correspond to
720
+ # line starts and paragraph starts; and determine their types.
721
+ self._train_tokens(self._tokenize_words(text), verbose)
722
+ if finalize:
723
+ self.finalize_training(verbose)
724
+
725
+ def train_tokens(self, tokens, verbose=False, finalize=True):
726
+ """
727
+ Collects training data from a given list of tokens.
728
+ """
729
+ self._train_tokens((self._Token(t) for t in tokens), verbose)
730
+ if finalize:
731
+ self.finalize_training(verbose)
732
+
733
+ def _train_tokens(self, tokens, verbose):
734
+ self._finalized = False
735
+
736
+ # Ensure tokens are a list
737
+ tokens = list(tokens)
738
+
739
+ # Find the frequency of each case-normalized type. (Don't
740
+ # strip off final periods.) Also keep track of the number of
741
+ # tokens that end in periods.
742
+ for aug_tok in tokens:
743
+ self._type_fdist[aug_tok.type] += 1
744
+ if aug_tok.period_final:
745
+ self._num_period_toks += 1
746
+
747
+ # Look for new abbreviations, and for types that no longer are
748
+ unique_types = self._unique_types(tokens)
749
+ for abbr, score, is_add in self._reclassify_abbrev_types(unique_types):
750
+ if score >= self.ABBREV:
751
+ if is_add:
752
+ self._params.abbrev_types.add(abbr)
753
+ if verbose:
754
+ print((' Abbreviation: [%6.4f] %s' %
755
+ (score, abbr)))
756
+ else:
757
+ if not is_add:
758
+ self._params.abbrev_types.remove(abbr)
759
+ if verbose:
760
+ print((' Removed abbreviation: [%6.4f] %s' %
761
+ (score, abbr)))
762
+
763
+ # Make a preliminary pass through the document, marking likely
764
+ # sentence breaks, abbreviations, and ellipsis tokens.
765
+ tokens = list(self._annotate_first_pass(tokens))
766
+
767
+ # Check what contexts each word type can appear in, given the
768
+ # case of its first letter.
769
+ self._get_orthography_data(tokens)
770
+
771
+ # We need total number of sentence breaks to find sentence starters
772
+ self._sentbreak_count += self._get_sentbreak_count(tokens)
773
+
774
+ # The remaining heuristics relate to pairs of tokens where the first
775
+ # ends in a period.
776
+ for aug_tok1, aug_tok2 in _pair_iter(tokens):
777
+ if not aug_tok1.period_final or not aug_tok2:
778
+ continue
779
+
780
+ # Is the first token a rare abbreviation?
781
+ if self._is_rare_abbrev_type(aug_tok1, aug_tok2):
782
+ self._params.abbrev_types.add(aug_tok1.type_no_period)
783
+ if verbose:
784
+ print((' Rare Abbrev: %s' % aug_tok1.type))
785
+
786
+ # Does second token have a high likelihood of starting a sentence?
787
+ if self._is_potential_sent_starter(aug_tok2, aug_tok1):
788
+ self._sent_starter_fdist[aug_tok2.type] += 1
789
+
790
+ # Is this bigram a potential collocation?
791
+ if self._is_potential_collocation(aug_tok1, aug_tok2):
792
+ self._collocation_fdist[
793
+ (aug_tok1.type_no_period, aug_tok2.type_no_sentperiod)] += 1
794
+
795
+ def _unique_types(self, tokens):
796
+ return set(aug_tok.type for aug_tok in tokens)
797
+
798
+ def finalize_training(self, verbose=False):
799
+ """
800
+ Uses data that has been gathered in training to determine likely
801
+ collocations and sentence starters.
802
+ """
803
+ self._params.clear_sent_starters()
804
+ for typ, ll in self._find_sent_starters():
805
+ self._params.sent_starters.add(typ)
806
+ if verbose:
807
+ print((' Sent Starter: [%6.4f] %r' % (ll, typ)))
808
+
809
+ self._params.clear_collocations()
810
+ for (typ1, typ2), ll in self._find_collocations():
811
+ self._params.collocations.add((typ1, typ2))
812
+ if verbose:
813
+ print((' Collocation: [%6.4f] %r+%r' %
814
+ (ll, typ1, typ2)))
815
+
816
+ self._finalized = True
817
+
818
+ # ////////////////////////////////////////////////////////////
819
+ # { Overhead reduction
820
+ # ////////////////////////////////////////////////////////////
821
+
822
+ def freq_threshold(self, ortho_thresh=2, type_thresh=2, colloc_thres=2,
823
+ sentstart_thresh=2):
824
+ """
825
+ Allows memory use to be reduced after much training by removing data
826
+ about rare tokens that are unlikely to have a statistical effect with
827
+ further training. Entries occurring above the given thresholds will be
828
+ retained.
829
+ """
830
+ if ortho_thresh > 1:
831
+ old_oc = self._params.ortho_context
832
+ self._params.clear_ortho_context()
833
+ for tok in self._type_fdist:
834
+ count = self._type_fdist[tok]
835
+ if count >= ortho_thresh:
836
+ self._params.ortho_context[tok] = old_oc[tok]
837
+
838
+ self._type_fdist = self._freq_threshold(self._type_fdist, type_thresh)
839
+ self._collocation_fdist = self._freq_threshold(
840
+ self._collocation_fdist, colloc_thres)
841
+ self._sent_starter_fdist = self._freq_threshold(
842
+ self._sent_starter_fdist, sentstart_thresh)
843
+
844
+ def _freq_threshold(self, fdist, threshold):
845
+ """
846
+ Returns a FreqDist containing only data with counts below a given
847
+ threshold, as well as a mapping (None -> count_removed).
848
+ """
849
+ # We assume that there is more data below the threshold than above it
850
+ # and so create a new FreqDist rather than working in place.
851
+ res = FreqDist()
852
+ num_removed = 0
853
+ for tok in fdist:
854
+ count = fdist[tok]
855
+ if count < threshold:
856
+ num_removed += 1
857
+ else:
858
+ res[tok] += count
859
+ res[None] += num_removed
860
+ return res
861
+
862
+ # ////////////////////////////////////////////////////////////
863
+ # { Orthographic data
864
+ # ////////////////////////////////////////////////////////////
865
+
866
+ def _get_orthography_data(self, tokens):
867
+ """
868
+ Collect information about whether each token type occurs
869
+ with different case patterns (i) overall, (ii) at
870
+ sentence-initial positions, and (iii) at sentence-internal
871
+ positions.
872
+ """
873
+ # 'initial' or 'internal' or 'unknown'
874
+ context = 'internal'
875
+ tokens = list(tokens)
876
+
877
+ for aug_tok in tokens:
878
+ # If we encounter a paragraph break, then it's a good sign
879
+ # that it's a sentence break. But err on the side of
880
+ # caution (by not positing a sentence break) if we just
881
+ # saw an abbreviation.
882
+ if aug_tok.parastart and context != 'unknown':
883
+ context = 'initial'
884
+
885
+ # If we're at the beginning of a line, then we can't decide
886
+ # between 'internal' and 'initial'.
887
+ if aug_tok.linestart and context == 'internal':
888
+ context = 'unknown'
889
+
890
+ # Find the case-normalized type of the token. If it's a
891
+ # sentence-final token, strip off the period.
892
+ typ = aug_tok.type_no_sentperiod
893
+
894
+ # Update the orthographic context table.
895
+ flag = _ORTHO_MAP.get((context, aug_tok.first_case), 0)
896
+ if flag:
897
+ self._params.add_ortho_context(typ, flag)
898
+
899
+ # Decide whether the next word is at a sentence boundary.
900
+ if aug_tok.sentbreak:
901
+ if not (aug_tok.is_number or aug_tok.is_initial):
902
+ context = 'initial'
903
+ else:
904
+ context = 'unknown'
905
+ elif aug_tok.ellipsis or aug_tok.abbr:
906
+ context = 'unknown'
907
+ else:
908
+ context = 'internal'
909
+
910
+ # ////////////////////////////////////////////////////////////
911
+ # { Abbreviations
912
+ # ////////////////////////////////////////////////////////////
913
+
914
+ def _reclassify_abbrev_types(self, types):
915
+ """
916
+ (Re)classifies each given token if
917
+ - it is period-final and not a known abbreviation; or
918
+ - it is not period-final and is otherwise a known abbreviation
919
+ by checking whether its previous classification still holds according
920
+ to the heuristics of section 3.
921
+ Yields triples (abbr, score, is_add) where abbr is the type in question,
922
+ score is its log-likelihood with penalties applied, and is_add specifies
923
+ whether the present type is a candidate for inclusion or exclusion as an
924
+ abbreviation, such that:
925
+ - (is_add and score >= 0.3) suggests a new abbreviation; and
926
+ - (not is_add and score < 0.3) suggests excluding an abbreviation.
927
+ """
928
+ # (While one could recalculate abbreviations from all .-final tokens at
929
+ # every iteration, in cases requiring efficiency, the number of tokens
930
+ # in the present training document will be much less.)
931
+
932
+ for typ in types:
933
+ # Check some basic conditions, to rule out words that are
934
+ # clearly not abbrev_types.
935
+ if not _re_non_punct.search(typ) or typ == '##number##':
936
+ continue
937
+
938
+ if typ.endswith('.'):
939
+ if typ in self._params.abbrev_types:
940
+ continue
941
+ typ = typ[:-1]
942
+ is_add = True
943
+ else:
944
+ if typ not in self._params.abbrev_types:
945
+ continue
946
+ is_add = False
947
+
948
+ # Count how many periods & nonperiods are in the
949
+ # candidate.
950
+ num_periods = typ.count('.') + 1
951
+ num_nonperiods = len(typ) - num_periods + 1
952
+
953
+ # Let <a> be the candidate without the period, and <b>
954
+ # be the period. Find a log likelihood ratio that
955
+ # indicates whether <ab> occurs as a single unit (high
956
+ # value of ll), or as two independent units <a> and
957
+ # <b> (low value of ll).
958
+ count_with_period = self._type_fdist[typ + '.']
959
+ count_without_period = self._type_fdist[typ]
960
+ ll = self._dunning_log_likelihood(
961
+ count_with_period + count_without_period,
962
+ self._num_period_toks, count_with_period,
963
+ self._type_fdist.N())
964
+
965
+ # Apply three scaling factors to 'tweak' the basic log
966
+ # likelihood ratio:
967
+ # F_length: long word -> less likely to be an abbrev
968
+ # F_periods: more periods -> more likely to be an abbrev
969
+ # F_penalty: penalize occurrences w/o a period
970
+ f_length = math.exp(-num_nonperiods)
971
+ f_periods = num_periods
972
+ f_penalty = (int(self.IGNORE_ABBREV_PENALTY)
973
+ or math.pow(num_nonperiods, -count_without_period))
974
+ score = ll * f_length * f_periods * f_penalty
975
+
976
+ yield typ, score, is_add
977
+
978
+ def find_abbrev_types(self):
979
+ """
980
+ Recalculates abbreviations given type frequencies, despite no prior
981
+ determination of abbreviations.
982
+ This fails to include abbreviations otherwise found as "rare".
983
+ """
984
+ self._params.clear_abbrevs()
985
+ tokens = (typ for typ in self._type_fdist if typ and typ.endswith('.'))
986
+ for abbr, score, is_add in self._reclassify_abbrev_types(tokens):
987
+ if score >= self.ABBREV:
988
+ self._params.abbrev_types.add(abbr)
989
+
990
+ # This function combines the work done by the original code's
991
+ # functions `count_orthography_context`, `get_orthography_count`,
992
+ # and `get_rare_abbreviations`.
993
+ def _is_rare_abbrev_type(self, cur_tok, next_tok):
994
+ """
995
+ A word type is counted as a rare abbreviation if...
996
+ - it's not already marked as an abbreviation
997
+ - it occurs fewer than ABBREV_BACKOFF times
998
+ - either it is followed by a sentence-internal punctuation
999
+ mark, *or* it is followed by a lower-case word that
1000
+ sometimes appears with upper case, but never occurs with
1001
+ lower case at the beginning of sentences.
1002
+ """
1003
+ if cur_tok.abbr or not cur_tok.sentbreak:
1004
+ return False
1005
+
1006
+ # Find the case-normalized type of the token. If it's
1007
+ # a sentence-final token, strip off the period.
1008
+ typ = cur_tok.type_no_sentperiod
1009
+
1010
+ # Proceed only if the type hasn't been categorized as an
1011
+ # abbreviation already, and is sufficiently rare...
1012
+ count = self._type_fdist[typ] + self._type_fdist[typ[:-1]]
1013
+ if (typ in self._params.abbrev_types or count >= self.ABBREV_BACKOFF):
1014
+ return False
1015
+
1016
+ # Record this token as an abbreviation if the next
1017
+ # token is a sentence-internal punctuation mark.
1018
+ # [XX] :1 or check the whole thing??
1019
+ if next_tok.tok[:1] in self._lang_vars.internal_punctuation:
1020
+ return True
1021
+
1022
+ # Record this type as an abbreviation if the next
1023
+ # token... (i) starts with a lower case letter,
1024
+ # (ii) sometimes occurs with an uppercase letter,
1025
+ # and (iii) never occus with an uppercase letter
1026
+ # sentence-internally.
1027
+ # [xx] should the check for (ii) be modified??
1028
+ elif next_tok.first_lower:
1029
+ typ2 = next_tok.type_no_sentperiod
1030
+ typ2ortho_context = self._params.ortho_context[typ2]
1031
+ if ((typ2ortho_context & _ORTHO_BEG_UC) and
1032
+ not (typ2ortho_context & _ORTHO_MID_UC)):
1033
+ return True
1034
+
1035
+ # ////////////////////////////////////////////////////////////
1036
+ # { Log Likelihoods
1037
+ # ////////////////////////////////////////////////////////////
1038
+
1039
+ # helper for _reclassify_abbrev_types:
1040
+ @staticmethod
1041
+ def _dunning_log_likelihood(count_a, count_b, count_ab, N):
1042
+ """
1043
+ A function that calculates the modified Dunning log-likelihood
1044
+ ratio scores for abbreviation candidates. The details of how
1045
+ this works is available in the paper.
1046
+ """
1047
+ p1 = count_b / N
1048
+ p2 = 0.99
1049
+
1050
+ null_hypo = (count_ab * math.log(p1) +
1051
+ (count_a - count_ab) * math.log(1.0 - p1))
1052
+ alt_hypo = (count_ab * math.log(p2) +
1053
+ (count_a - count_ab) * math.log(1.0 - p2))
1054
+
1055
+ likelihood = null_hypo - alt_hypo
1056
+
1057
+ return (-2.0 * likelihood)
1058
+
1059
+ @staticmethod
1060
+ def _col_log_likelihood(count_a, count_b, count_ab, N):
1061
+ """
1062
+ A function that will just compute log-likelihood estimate, in
1063
+ the original paper it's described in algorithm 6 and 7.
1064
+
1065
+ This *should* be the original Dunning log-likelihood values,
1066
+ unlike the previous log_l function where it used modified
1067
+ Dunning log-likelihood values
1068
+ """
1069
+ p = count_b / N
1070
+ p1 = count_ab / count_a
1071
+ try:
1072
+ p2 = (count_b - count_ab) / (N - count_a)
1073
+ except ZeroDivisionError as e:
1074
+ p2 = 1
1075
+
1076
+ try:
1077
+ summand1 = (count_ab * math.log(p) +
1078
+ (count_a - count_ab) * math.log(1.0 - p))
1079
+ except ValueError as e:
1080
+ summand1 = 0
1081
+
1082
+ try:
1083
+ summand2 = ((count_b - count_ab) * math.log(p) +
1084
+ (N - count_a - count_b + count_ab) * math.log(1.0 - p))
1085
+ except ValueError as e:
1086
+ summand2 = 0
1087
+
1088
+ if count_a == count_ab or p1 <= 0 or p1 >= 1:
1089
+ summand3 = 0
1090
+ else:
1091
+ summand3 = (count_ab * math.log(p1) +
1092
+ (count_a - count_ab) * math.log(1.0 - p1))
1093
+
1094
+ if count_b == count_ab or p2 <= 0 or p2 >= 1:
1095
+ summand4 = 0
1096
+ else:
1097
+ summand4 = ((count_b - count_ab) * math.log(p2) +
1098
+ (N - count_a - count_b + count_ab) * math.log(1.0 - p2))
1099
+
1100
+ likelihood = summand1 + summand2 - summand3 - summand4
1101
+
1102
+ return (-2.0 * likelihood)
1103
+
1104
+ # ////////////////////////////////////////////////////////////
1105
+ # { Collocation Finder
1106
+ # ////////////////////////////////////////////////////////////
1107
+
1108
+ def _is_potential_collocation(self, aug_tok1, aug_tok2):
1109
+ """
1110
+ Returns True if the pair of tokens may form a collocation given
1111
+ log-likelihood statistics.
1112
+ """
1113
+ return ((self.INCLUDE_ALL_COLLOCS or
1114
+ (self.INCLUDE_ABBREV_COLLOCS and aug_tok1.abbr) or
1115
+ (aug_tok1.sentbreak and
1116
+ (aug_tok1.is_number or aug_tok1.is_initial)))
1117
+ and aug_tok1.is_non_punct
1118
+ and aug_tok2.is_non_punct)
1119
+
1120
+ def _find_collocations(self):
1121
+ """
1122
+ Generates likely collocations and their log-likelihood.
1123
+ """
1124
+ for types in self._collocation_fdist:
1125
+ try:
1126
+ typ1, typ2 = types
1127
+ except TypeError:
1128
+ # types may be None after calling freq_threshold()
1129
+ continue
1130
+ if typ2 in self._params.sent_starters:
1131
+ continue
1132
+
1133
+ col_count = self._collocation_fdist[types]
1134
+ typ1_count = self._type_fdist[typ1] + self._type_fdist[typ1 + '.']
1135
+ typ2_count = self._type_fdist[typ2] + self._type_fdist[typ2 + '.']
1136
+ if (typ1_count > 1 and typ2_count > 1
1137
+ and self.MIN_COLLOC_FREQ <
1138
+ col_count <= min(typ1_count, typ2_count)):
1139
+
1140
+ ll = self._col_log_likelihood(typ1_count, typ2_count,
1141
+ col_count, self._type_fdist.N())
1142
+ # Filter out the not-so-collocative
1143
+ if (ll >= self.COLLOCATION and
1144
+ (self._type_fdist.N() / typ1_count >
1145
+ typ2_count / col_count)):
1146
+ yield (typ1, typ2), ll
1147
+
1148
+ # ////////////////////////////////////////////////////////////
1149
+ # { Sentence-Starter Finder
1150
+ # ////////////////////////////////////////////////////////////
1151
+
1152
+ def _is_potential_sent_starter(self, cur_tok, prev_tok):
1153
+ """
1154
+ Returns True given a token and the token that preceds it if it
1155
+ seems clear that the token is beginning a sentence.
1156
+ """
1157
+ # If a token (i) is preceded by a sentece break that is
1158
+ # not a potential ordinal number or initial, and (ii) is
1159
+ # alphabetic, then it is a a sentence-starter.
1160
+ return (prev_tok.sentbreak and
1161
+ not (prev_tok.is_number or prev_tok.is_initial) and
1162
+ cur_tok.is_alpha)
1163
+
1164
+ def _find_sent_starters(self):
1165
+ """
1166
+ Uses collocation heuristics for each candidate token to
1167
+ determine if it frequently starts sentences.
1168
+ """
1169
+ for typ in self._sent_starter_fdist:
1170
+ if not typ:
1171
+ continue
1172
+
1173
+ typ_at_break_count = self._sent_starter_fdist[typ]
1174
+ typ_count = self._type_fdist[typ] + self._type_fdist[typ + '.']
1175
+ if typ_count < typ_at_break_count:
1176
+ # needed after freq_threshold
1177
+ continue
1178
+
1179
+ ll = self._col_log_likelihood(self._sentbreak_count, typ_count,
1180
+ typ_at_break_count,
1181
+ self._type_fdist.N())
1182
+
1183
+ if (ll >= self.SENT_STARTER and
1184
+ self._type_fdist.N() / self._sentbreak_count >
1185
+ typ_count / typ_at_break_count):
1186
+ yield typ, ll
1187
+
1188
+ def _get_sentbreak_count(self, tokens):
1189
+ """
1190
+ Returns the number of sentence breaks marked in a given set of
1191
+ augmented tokens.
1192
+ """
1193
+ return sum(1 for aug_tok in tokens if aug_tok.sentbreak)
1194
+
1195
+
1196
+ ######################################################################
1197
+ # { Punkt Sentence Tokenizer
1198
+ ######################################################################
1199
+
1200
+
1201
+ class PunktSentenceTokenizer(PunktBaseClass, TokenizerI):
1202
+ """
1203
+ A sentence tokenizer which uses an unsupervised algorithm to build
1204
+ a model for abbreviation words, collocations, and words that start
1205
+ sentences; and then uses that model to find sentence boundaries.
1206
+ This approach has been shown to work well for many European
1207
+ languages.
1208
+ """
1209
+
1210
+ def __init__(self, train_text=None, verbose=False,
1211
+ lang_vars=PunktLanguageVars(), token_cls=PunktToken):
1212
+ """
1213
+ train_text can either be the sole training text for this sentence
1214
+ boundary detector, or can be a PunktParameters object.
1215
+ """
1216
+ PunktBaseClass.__init__(self, lang_vars=lang_vars,
1217
+ token_cls=token_cls)
1218
+
1219
+ if train_text:
1220
+ self._params = self.train(train_text, verbose)
1221
+
1222
+ def train(self, train_text, verbose=False):
1223
+ """
1224
+ Derives parameters from a given training text, or uses the parameters
1225
+ given. Repeated calls to this method destroy previous parameters. For
1226
+ incremental training, instantiate a separate PunktTrainer instance.
1227
+ """
1228
+ if not isinstance(train_text, string_types):
1229
+ return train_text
1230
+ return PunktTrainer(train_text, lang_vars=self._lang_vars,
1231
+ token_cls=self._Token).get_params()
1232
+
1233
+ # ////////////////////////////////////////////////////////////
1234
+ # { Tokenization
1235
+ # ////////////////////////////////////////////////////////////
1236
+
1237
+ def tokenize(self, text, realign_boundaries=True):
1238
+ """
1239
+ Given a text, returns a list of the sentences in that text.
1240
+ """
1241
+ return list(self.sentences_from_text(text, realign_boundaries))
1242
+
1243
+ def debug_decisions(self, text):
1244
+ """
1245
+ Classifies candidate periods as sentence breaks, yielding a dict for
1246
+ each that may be used to understand why the decision was made.
1247
+
1248
+ See format_debug_decision() to help make this output readable.
1249
+ """
1250
+
1251
+ for match in self._lang_vars.period_context_re().finditer(text):
1252
+ decision_text = match.group() + match.group('after_tok')
1253
+ tokens = self._tokenize_words(decision_text)
1254
+ tokens = list(self._annotate_first_pass(tokens))
1255
+ while not tokens[0].period_final:
1256
+ tokens.pop(0)
1257
+ yield dict(period_index=match.end() - 1,
1258
+ text=decision_text,
1259
+ type1=tokens[0].type,
1260
+ type2=tokens[1].type,
1261
+ type1_in_abbrs=bool(tokens[0].abbr),
1262
+ type1_is_initial=bool(tokens[0].is_initial),
1263
+ type2_is_sent_starter=tokens[1].type_no_sentperiod in self._params.sent_starters,
1264
+ type2_ortho_heuristic=self._ortho_heuristic(tokens[1]),
1265
+ type2_ortho_contexts=set(self._params._debug_ortho_context(tokens[1].type_no_sentperiod)),
1266
+ collocation=(tokens[0].type_no_sentperiod,
1267
+ tokens[1].type_no_sentperiod) in self._params.collocations,
1268
+
1269
+ reason=self._second_pass_annotation(tokens[0], tokens[1]) or REASON_DEFAULT_DECISION,
1270
+ break_decision=tokens[0].sentbreak,
1271
+ )
1272
+
1273
+ def span_tokenize(self, text, realign_boundaries=True):
1274
+ """
1275
+ Given a text, generates (start, end) spans of sentences
1276
+ in the text.
1277
+ """
1278
+ slices = self._slices_from_text(text)
1279
+ if realign_boundaries:
1280
+ slices = self._realign_boundaries(text, slices)
1281
+ for sl in slices:
1282
+ yield (sl.start, sl.stop)
1283
+
1284
+ def sentences_from_text(self, text, realign_boundaries=True):
1285
+ """
1286
+ Given a text, generates the sentences in that text by only
1287
+ testing candidate sentence breaks. If realign_boundaries is
1288
+ True, includes in the sentence closing punctuation that
1289
+ follows the period.
1290
+ """
1291
+ return [text[s:e] for s, e in self.span_tokenize(text, realign_boundaries)]
1292
+
1293
+ def _slices_from_text(self, text):
1294
+ last_break = 0
1295
+ for match in self._lang_vars.period_context_re().finditer(text):
1296
+ context = match.group() + match.group('after_tok')
1297
+ if self.text_contains_sentbreak(context):
1298
+ yield slice(last_break, match.end())
1299
+ if match.group('next_tok'):
1300
+ # next sentence starts after whitespace
1301
+ last_break = match.start('next_tok')
1302
+ else:
1303
+ # next sentence starts at following punctuation
1304
+ last_break = match.end()
1305
+ # The last sentence should not contain trailing whitespace.
1306
+ yield slice(last_break, len(text.rstrip()))
1307
+
1308
+ def _realign_boundaries(self, text, slices):
1309
+ """
1310
+ Attempts to realign punctuation that falls after the period but
1311
+ should otherwise be included in the same sentence.
1312
+
1313
+ For example: "(Sent1.) Sent2." will otherwise be split as::
1314
+
1315
+ ["(Sent1.", ") Sent1."].
1316
+
1317
+ This method will produce::
1318
+
1319
+ ["(Sent1.)", "Sent2."].
1320
+ """
1321
+ realign = 0
1322
+ for sl1, sl2 in _pair_iter(slices):
1323
+ sl1 = slice(sl1.start + realign, sl1.stop)
1324
+ if not sl2:
1325
+ if text[sl1]:
1326
+ yield sl1
1327
+ continue
1328
+
1329
+ m = self._lang_vars.re_boundary_realignment.match(text[sl2])
1330
+ if m:
1331
+ yield slice(sl1.start, sl2.start + len(m.group(0).rstrip()))
1332
+ realign = m.end()
1333
+ else:
1334
+ realign = 0
1335
+ if text[sl1]:
1336
+ yield sl1
1337
+
1338
+ def text_contains_sentbreak(self, text):
1339
+ """
1340
+ Returns True if the given text includes a sentence break.
1341
+ """
1342
+ found = False # used to ignore last token
1343
+ for t in self._annotate_tokens(self._tokenize_words(text)):
1344
+ if found:
1345
+ return True
1346
+ if t.sentbreak:
1347
+ found = True
1348
+ return False
1349
+
1350
+ def sentences_from_text_legacy(self, text):
1351
+ """
1352
+ Given a text, generates the sentences in that text. Annotates all
1353
+ tokens, rather than just those with possible sentence breaks. Should
1354
+ produce the same results as ``sentences_from_text``.
1355
+ """
1356
+ tokens = self._annotate_tokens(self._tokenize_words(text))
1357
+ return self._build_sentence_list(text, tokens)
1358
+
1359
+ def sentences_from_tokens(self, tokens):
1360
+ """
1361
+ Given a sequence of tokens, generates lists of tokens, each list
1362
+ corresponding to a sentence.
1363
+ """
1364
+ tokens = iter(self._annotate_tokens(self._Token(t) for t in tokens))
1365
+ sentence = []
1366
+ for aug_tok in tokens:
1367
+ sentence.append(aug_tok.tok)
1368
+ if aug_tok.sentbreak:
1369
+ yield sentence
1370
+ sentence = []
1371
+ if sentence:
1372
+ yield sentence
1373
+
1374
+ def _annotate_tokens(self, tokens):
1375
+ """
1376
+ Given a set of tokens augmented with markers for line-start and
1377
+ paragraph-start, returns an iterator through those tokens with full
1378
+ annotation including predicted sentence breaks.
1379
+ """
1380
+ # Make a preliminary pass through the document, marking likely
1381
+ # sentence breaks, abbreviations, and ellipsis tokens.
1382
+ tokens = self._annotate_first_pass(tokens)
1383
+
1384
+ # Make a second pass through the document, using token context
1385
+ # information to change our preliminary decisions about where
1386
+ # sentence breaks, abbreviations, and ellipsis occurs.
1387
+ tokens = self._annotate_second_pass(tokens)
1388
+
1389
+ ## [XX] TESTING
1390
+ # tokens = list(tokens)
1391
+ # self.dump(tokens)
1392
+
1393
+ return tokens
1394
+
1395
+ def _build_sentence_list(self, text, tokens):
1396
+ """
1397
+ Given the original text and the list of augmented word tokens,
1398
+ construct and return a tokenized list of sentence strings.
1399
+ """
1400
+ # Most of the work here is making sure that we put the right
1401
+ # pieces of whitespace back in all the right places.
1402
+
1403
+ # Our position in the source text, used to keep track of which
1404
+ # whitespace to add:
1405
+ pos = 0
1406
+
1407
+ # A regular expression that finds pieces of whitespace:
1408
+ WS_REGEXP = re.compile(r'\s*')
1409
+
1410
+ sentence = ''
1411
+ for aug_tok in tokens:
1412
+ tok = aug_tok.tok
1413
+
1414
+ # Find the whitespace before this token, and update pos.
1415
+ ws = WS_REGEXP.match(text, pos).group()
1416
+ pos += len(ws)
1417
+
1418
+ # Some of the rules used by the punkt word tokenizer
1419
+ # strip whitespace out of the text, resulting in tokens
1420
+ # that contain whitespace in the source text. If our
1421
+ # token doesn't match, see if adding whitespace helps.
1422
+ # If so, then use the version with whitespace.
1423
+ if text[pos:pos + len(tok)] != tok:
1424
+ pat = '\s*'.join(re.escape(c) for c in tok)
1425
+ m = re.compile(pat).match(text, pos)
1426
+ if m: tok = m.group()
1427
+
1428
+ # Move our position pointer to the end of the token.
1429
+ assert text[pos:pos + len(tok)] == tok
1430
+ pos += len(tok)
1431
+
1432
+ # Add this token. If it's not at the beginning of the
1433
+ # sentence, then include any whitespace that separated it
1434
+ # from the previous token.
1435
+ if sentence:
1436
+ sentence += ws
1437
+ sentence += tok
1438
+
1439
+ # If we're at a sentence break, then start a new sentence.
1440
+ if aug_tok.sentbreak:
1441
+ yield sentence
1442
+ sentence = ''
1443
+
1444
+ # If the last sentence is emtpy, discard it.
1445
+ if sentence:
1446
+ yield sentence
1447
+
1448
+ # [XX] TESTING
1449
+ def dump(self, tokens):
1450
+ print('writing to /tmp/punkt.new...')
1451
+ with open('/tmp/punkt.new', 'w') as outfile:
1452
+ for aug_tok in tokens:
1453
+ if aug_tok.parastart:
1454
+ outfile.write('\n\n')
1455
+ elif aug_tok.linestart:
1456
+ outfile.write('\n')
1457
+ else:
1458
+ outfile.write(' ')
1459
+
1460
+ outfile.write(str(aug_tok))
1461
+
1462
+ # ////////////////////////////////////////////////////////////
1463
+ # { Customization Variables
1464
+ # ////////////////////////////////////////////////////////////
1465
+
1466
+ PUNCTUATION = tuple(';:,.!?')
1467
+
1468
+ # ////////////////////////////////////////////////////////////
1469
+ # { Annotation Procedures
1470
+ # ////////////////////////////////////////////////////////////
1471
+
1472
+ def _annotate_second_pass(self, tokens):
1473
+ """
1474
+ Performs a token-based classification (section 4) over the given
1475
+ tokens, making use of the orthographic heuristic (4.1.1), collocation
1476
+ heuristic (4.1.2) and frequent sentence starter heuristic (4.1.3).
1477
+ """
1478
+ for t1, t2 in _pair_iter(tokens):
1479
+ self._second_pass_annotation(t1, t2)
1480
+ yield t1
1481
+
1482
+ def _second_pass_annotation(self, aug_tok1, aug_tok2):
1483
+ """
1484
+ Performs token-based classification over a pair of contiguous tokens
1485
+ updating the first.
1486
+ """
1487
+ # Is it the last token? We can't do anything then.
1488
+ if not aug_tok2:
1489
+ return
1490
+
1491
+ tok = aug_tok1.tok
1492
+ if not aug_tok1.period_final:
1493
+ # We only care about words ending in periods.
1494
+ return
1495
+
1496
+ typ = aug_tok1.type_no_period
1497
+ next_tok = aug_tok2.tok
1498
+ next_typ = aug_tok2.type_no_sentperiod
1499
+ tok_is_initial = aug_tok1.is_initial
1500
+
1501
+ # [4.1.2. Collocation Heuristic] If there's a
1502
+ # collocation between the word before and after the
1503
+ # period, then label tok as an abbreviation and NOT
1504
+ # a sentence break. Note that collocations with
1505
+ # frequent sentence starters as their second word are
1506
+ # excluded in training.
1507
+ if (typ, next_typ) in self._params.collocations:
1508
+ aug_tok1.sentbreak = False
1509
+ aug_tok1.abbr = True
1510
+ return REASON_KNOWN_COLLOCATION
1511
+
1512
+ # [4.2. Token-Based Reclassification of Abbreviations] If
1513
+ # the token is an abbreviation or an ellipsis, then decide
1514
+ # whether we should *also* classify it as a sentbreak.
1515
+ if ((aug_tok1.abbr or aug_tok1.ellipsis) and
1516
+ (not tok_is_initial)):
1517
+ # [4.1.1. Orthographic Heuristic] Check if there's
1518
+ # orthogrpahic evidence about whether the next word
1519
+ # starts a sentence or not.
1520
+ is_sent_starter = self._ortho_heuristic(aug_tok2)
1521
+ if is_sent_starter == True:
1522
+ aug_tok1.sentbreak = True
1523
+ return REASON_ABBR_WITH_ORTHOGRAPHIC_HEURISTIC
1524
+
1525
+ # [4.1.3. Frequent Sentence Starter Heruistic] If the
1526
+ # next word is capitalized, and is a member of the
1527
+ # frequent-sentence-starters list, then label tok as a
1528
+ # sentence break.
1529
+ if (aug_tok2.first_upper and
1530
+ next_typ in self._params.sent_starters):
1531
+ aug_tok1.sentbreak = True
1532
+ return REASON_ABBR_WITH_SENTENCE_STARTER
1533
+
1534
+ # [4.3. Token-Based Detection of Initials and Ordinals]
1535
+ # Check if any initials or ordinals tokens that are marked
1536
+ # as sentbreaks should be reclassified as abbreviations.
1537
+ if tok_is_initial or typ == '##number##':
1538
+
1539
+ # [4.1.1. Orthographic Heuristic] Check if there's
1540
+ # orthogrpahic evidence about whether the next word
1541
+ # starts a sentence or not.
1542
+ is_sent_starter = self._ortho_heuristic(aug_tok2)
1543
+
1544
+ if is_sent_starter == False:
1545
+ aug_tok1.sentbreak = False
1546
+ aug_tok1.abbr = True
1547
+ if tok_is_initial:
1548
+ return REASON_INITIAL_WITH_ORTHOGRAPHIC_HEURISTIC
1549
+ else:
1550
+ return REASON_NUMBER_WITH_ORTHOGRAPHIC_HEURISTIC
1551
+
1552
+ # Special heuristic for initials: if orthogrpahic
1553
+ # heuristc is unknown, and next word is always
1554
+ # capitalized, then mark as abbrev (eg: J. Bach).
1555
+ if (is_sent_starter == 'unknown' and tok_is_initial and
1556
+ aug_tok2.first_upper and
1557
+ not (self._params.ortho_context[next_typ] & _ORTHO_LC)):
1558
+ aug_tok1.sentbreak = False
1559
+ aug_tok1.abbr = True
1560
+ return REASON_INITIAL_WITH_SPECIAL_ORTHOGRAPHIC_HEURISTIC
1561
+
1562
+ return
1563
+
1564
+ def _ortho_heuristic(self, aug_tok):
1565
+ """
1566
+ Decide whether the given token is the first token in a sentence.
1567
+ """
1568
+ # Sentences don't start with punctuation marks:
1569
+ if aug_tok.tok in self.PUNCTUATION:
1570
+ return False
1571
+
1572
+ ortho_context = self._params.ortho_context[aug_tok.type_no_sentperiod]
1573
+
1574
+ # If the word is capitalized, occurs at least once with a
1575
+ # lower case first letter, and never occurs with an upper case
1576
+ # first letter sentence-internally, then it's a sentence starter.
1577
+ if (aug_tok.first_upper and
1578
+ (ortho_context & _ORTHO_LC) and
1579
+ not (ortho_context & _ORTHO_MID_UC)):
1580
+ return True
1581
+
1582
+ # If the word is lower case, and either (a) we've seen it used
1583
+ # with upper case, or (b) we've never seen it used
1584
+ # sentence-initially with lower case, then it's not a sentence
1585
+ # starter.
1586
+ if (aug_tok.first_lower and
1587
+ ((ortho_context & _ORTHO_UC) or
1588
+ not (ortho_context & _ORTHO_BEG_LC))):
1589
+ return False
1590
+
1591
+ # Otherwise, we're not sure.
1592
+ return 'unknown'
1593
+
1594
+
1595
+ DEBUG_DECISION_FMT = '''Text: %(text)r (at offset %(period_index)d)
1596
+ Sentence break? %(break_decision)s (%(reason)s)
1597
+ Collocation? %(collocation)s
1598
+ %(type1)r:
1599
+ known abbreviation: %(type1_in_abbrs)s
1600
+ is initial: %(type1_is_initial)s
1601
+ %(type2)r:
1602
+ known sentence starter: %(type2_is_sent_starter)s
1603
+ orthographic heuristic suggests is a sentence starter? %(type2_ortho_heuristic)s
1604
+ orthographic contexts in training: %(type2_ortho_contexts)s
1605
+ '''
1606
+
1607
+
1608
+ def format_debug_decision(d):
1609
+ return DEBUG_DECISION_FMT % d
1610
+
1611
+
1612
+ def demo(text, tok_cls=PunktSentenceTokenizer, train_cls=PunktTrainer):
1613
+ """Builds a punkt model and applies it to the same text"""
1614
+ cleanup = lambda s: re.compile(r'(?:\r|^\s+)', re.MULTILINE).sub('', s).replace('\n', ' ')
1615
+ trainer = train_cls()
1616
+ trainer.INCLUDE_ALL_COLLOCS = True
1617
+ trainer.train(text)
1618
+ sbd = tok_cls(trainer.get_params())
1619
+ for l in sbd.sentences_from_text(text):
1620
+ print(cleanup(l))