spaCy/spacy/displacy/__init__.py

217 lines
7.7 KiB
Python
Raw Normal View History

"""
spaCy's built in visualization suite for dependencies and named entities.
DOCS: https://spacy.io/api/top-level#displacy
USAGE: https://spacy.io/usage/visualizers
"""
2020-07-25 13:01:15 +00:00
from typing import Union, Iterable, Optional, Dict, Any, Callable
2020-02-28 11:20:23 +00:00
import warnings
2017-05-14 15:50:23 +00:00
from .render import DependencyRenderer, EntityRenderer
from ..tokens import Doc, Span
2020-02-28 11:20:23 +00:00
from ..errors import Errors, Warnings
💫 New JSON helpers, training data internals & CLI rewrite (#2932) * Support nowrap setting in util.prints * Tidy up and fix whitespace * Simplify script and use read_jsonl helper * Add JSON schemas (see #2928) * Deprecate Doc.print_tree Will be replaced with Doc.to_json, which will produce a unified format * Add Doc.to_json() method (see #2928) Converts Doc objects to JSON using the same unified format as the training data. Method also supports serializing selected custom attributes in the doc._. space. * Remove outdated test * Add write_json and write_jsonl helpers * WIP: Update spacy train * Tidy up spacy train * WIP: Use wasabi for formatting * Add GoldParse helpers for JSON format * WIP: add debug-data command * Fix typo * Add missing import * Update wasabi pin * Add missing import * 💫 Refactor CLI (#2943) To be merged into #2932. ## Description - [x] refactor CLI To use [`wasabi`](https://github.com/ines/wasabi) - [x] use [`black`](https://github.com/ambv/black) for auto-formatting - [x] add `flake8` config - [x] move all messy UD-related scripts to `cli.ud` - [x] make converters function that take the opened file and return the converted data (instead of having them handle the IO) ### Types of change enhancement ## Checklist <!--- Before you submit the PR, go over this checklist and make sure you can tick off all the boxes. [] -> [x] --> - [x] I have submitted the spaCy Contributor Agreement. - [x] I ran the tests, and all new and existing tests passed. - [x] My changes don't require a change to the documentation, or if they do, I've added all required information. * Update wasabi pin * Delete old test * Update errors * Fix typo * Tidy up and format remaining code * Fix formatting * Improve formatting of messages * Auto-format remaining code * Add tok2vec stuff to spacy.train * Fix typo * Update wasabi pin * Fix path checks for when train() is called as function * Reformat and tidy up pretrain script * Update argument annotations * Raise error if model language doesn't match lang * Document new train command
2018-11-30 19:16:14 +00:00
from ..util import is_in_jupyter
2017-05-14 15:50:23 +00:00
_html = {}
RENDER_WRAPPER = None
2017-05-14 15:50:23 +00:00
def render(
2020-08-17 14:45:24 +00:00
docs: Union[Iterable[Union[Doc, Span]], Doc, Span],
2020-07-25 13:01:15 +00:00
style: str = "dep",
page: bool = False,
minify: bool = False,
jupyter: Optional[bool] = None,
options: Dict[str, Any] = {},
manual: bool = False,
) -> str:
2017-05-14 15:50:23 +00:00
"""Render displaCy visualisation.
2020-07-25 13:01:15 +00:00
docs (Union[Iterable[Doc], Doc]): Document(s) to visualise.
2020-05-24 15:20:58 +00:00
style (str): Visualisation style, 'dep' or 'ent'.
2017-05-14 15:50:23 +00:00
page (bool): Render markup as full HTML page.
minify (bool): Minify HTML markup.
jupyter (bool): Override Jupyter auto-detection.
2017-05-14 15:50:23 +00:00
options (dict): Visualiser-specific options, e.g. colors.
2017-10-27 12:39:19 +00:00
manual (bool): Don't parse `Doc` and instead expect a dict/list of dicts.
2020-05-24 15:20:58 +00:00
RETURNS (str): Rendered HTML markup.
DOCS: https://spacy.io/api/top-level#displacy.render
USAGE: https://spacy.io/usage/visualizers
2017-05-14 15:50:23 +00:00
"""
factories = {
"dep": (DependencyRenderer, parse_deps),
"ent": (EntityRenderer, parse_ents),
}
if style not in factories:
raise ValueError(Errors.E087.format(style=style))
if isinstance(docs, (Doc, Span, dict)):
docs = [docs]
docs = [obj if not isinstance(obj, Span) else obj.as_doc() for obj in docs]
if not all(isinstance(obj, (Doc, Span, dict)) for obj in docs):
raise ValueError(Errors.E096)
2020-07-25 13:01:15 +00:00
renderer_func, converter = factories[style]
renderer = renderer_func(options=options)
parsed = [converter(doc, options) for doc in docs] if not manual else docs
_html["parsed"] = renderer.render(parsed, page=page, minify=minify).strip()
html = _html["parsed"]
if RENDER_WRAPPER is not None:
html = RENDER_WRAPPER(html)
if jupyter or (jupyter is None and is_in_jupyter()):
# return HTML rendered by IPython display()
# See #4840 for details on span wrapper to disable mathjax
from IPython.core.display import display, HTML
return display(HTML('<span class="tex2jax_ignore">{}</span>'.format(html)))
return html
2017-05-14 15:50:23 +00:00
def serve(
2020-07-25 13:01:15 +00:00
docs: Union[Iterable[Doc], Doc],
style: str = "dep",
page: bool = True,
minify: bool = False,
options: Dict[str, Any] = {},
manual: bool = False,
port: int = 5000,
host: str = "0.0.0.0",
) -> None:
2017-05-14 15:50:23 +00:00
"""Serve displaCy visualisation.
docs (list or Doc): Document(s) to visualise.
2020-05-24 15:20:58 +00:00
style (str): Visualisation style, 'dep' or 'ent'.
2017-05-14 15:50:23 +00:00
page (bool): Render markup as full HTML page.
minify (bool): Minify HTML markup.
options (dict): Visualiser-specific options, e.g. colors.
2017-10-27 12:39:19 +00:00
manual (bool): Don't parse `Doc` and instead expect a dict/list of dicts.
2017-05-14 15:50:23 +00:00
port (int): Port to serve visualisation.
2020-05-24 15:20:58 +00:00
host (str): Host to serve visualisation.
DOCS: https://spacy.io/api/top-level#displacy.serve
USAGE: https://spacy.io/usage/visualizers
2017-05-14 15:50:23 +00:00
"""
from wsgiref import simple_server
2019-02-08 13:14:49 +00:00
if is_in_jupyter():
2020-02-28 11:20:23 +00:00
warnings.warn(Warnings.W011)
render(docs, style=style, page=page, minify=minify, options=options, manual=manual)
httpd = simple_server.make_server(host, port, app)
print(f"\nUsing the '{style}' visualizer")
print(f"Serving on http://{host}:{port} ...\n")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print(f"Shutting down server on port {port}.")
finally:
httpd.server_close()
2017-05-14 15:50:23 +00:00
def app(environ, start_response):
headers = [("Content-type", "text/html; charset=utf-8")]
start_response("200 OK", headers)
res = _html["parsed"].encode(encoding="utf-8")
2017-05-14 15:50:23 +00:00
return [res]
2020-07-25 13:01:15 +00:00
def parse_deps(orig_doc: Doc, options: Dict[str, Any] = {}) -> Dict[str, Any]:
2017-05-14 15:50:23 +00:00
"""Generate dependency parse in {'words': [], 'arcs': []} format.
doc (Doc): Document do parse.
RETURNS (dict): Generated dependency parse keyed by words and arcs.
"""
doc = Doc(orig_doc.vocab).from_bytes(orig_doc.to_bytes(exclude=["user_data"]))
if not doc.has_annotation("DEP"):
2020-02-28 11:20:23 +00:00
warnings.warn(Warnings.W005)
if options.get("collapse_phrases", False):
with doc.retokenize() as retokenizer:
for np in list(doc.noun_chunks):
attrs = {
"tag": np.root.tag_,
"lemma": np.root.lemma_,
"ent_type": np.root.ent_type_,
}
retokenizer.merge(np, attrs=attrs)
if options.get("collapse_punct", True):
2017-05-14 15:50:23 +00:00
spans = []
for word in doc[:-1]:
if word.is_punct or not word.nbor(1).is_punct:
continue
start = word.i
end = word.i + 1
while end < len(doc) and doc[end].is_punct:
end += 1
2017-10-27 12:39:19 +00:00
span = doc[start:end]
spans.append((span, word.tag_, word.lemma_, word.ent_type_))
with doc.retokenize() as retokenizer:
for span, tag, lemma, ent_type in spans:
attrs = {"tag": tag, "lemma": lemma, "ent_type": ent_type}
retokenizer.merge(span, attrs=attrs)
fine_grained = options.get("fine_grained")
add_lemma = options.get("add_lemma")
2020-03-25 11:28:12 +00:00
words = [
{
"text": w.text,
"tag": w.tag_ if fine_grained else w.pos_,
"lemma": w.lemma_ if add_lemma else None,
}
for w in doc
]
2017-05-14 15:50:23 +00:00
arcs = []
for word in doc:
if word.i < word.head.i:
arcs.append(
{"start": word.i, "end": word.head.i, "label": word.dep_, "dir": "left"}
)
2017-05-14 15:50:23 +00:00
elif word.i > word.head.i:
arcs.append(
{
"start": word.head.i,
"end": word.i,
"label": word.dep_,
"dir": "right",
}
)
return {"words": words, "arcs": arcs, "settings": get_doc_settings(orig_doc)}
2017-05-14 15:50:23 +00:00
2020-07-25 13:01:15 +00:00
def parse_ents(doc: Doc, options: Dict[str, Any] = {}) -> Dict[str, Any]:
2017-05-14 15:50:23 +00:00
"""Generate named entities in [{start: i, end: i, label: 'label'}] format.
doc (Doc): Document do parse.
RETURNS (dict): Generated entities keyed by text (original text) and ents.
"""
ents = [
{"start": ent.start_char, "end": ent.end_char, "label": ent.label_}
for ent in doc.ents
]
if not ents:
2020-02-28 11:20:23 +00:00
warnings.warn(Warnings.W006)
title = doc.user_data.get("title", None) if hasattr(doc, "user_data") else None
settings = get_doc_settings(doc)
return {"text": doc.text, "ents": ents, "title": title, "settings": settings}
2020-07-25 13:01:15 +00:00
def set_render_wrapper(func: Callable[[str], str]) -> None:
"""Set an optional wrapper function that is called around the generated
HTML markup on displacy.render. This can be used to allow integration into
other platforms, similar to Jupyter Notebooks that require functions to be
called around the HTML. It can also be used to implement custom callbacks
on render, or to embed the visualization in a custom page.
func (callable): Function to call around markup before rendering it. Needs
to take one argument, the HTML markup, and should return the desired
output of displacy.render.
"""
global RENDER_WRAPPER
if not hasattr(func, "__call__"):
raise ValueError(Errors.E110.format(obj=type(func)))
RENDER_WRAPPER = func
2020-07-25 13:01:15 +00:00
def get_doc_settings(doc: Doc) -> Dict[str, Any]:
return {
"lang": doc.lang_,
"direction": doc.vocab.writing_system.get("direction", "ltr"),
}