wuenlp_tools.visualize.scenes
1from __future__ import annotations 2 3from collections import defaultdict 4from pathlib import Path 5from typing import Optional, Type 6 7from loguru import logger 8from wuenlp.impl.UIMANLPStructs import UIMADocument, UIMASentence, UIMAToken, UIMAScene 9import plotly.graph_objs as go 10import plotly.io as pio 11 12import textwrap 13 14from wuenlp.impl.uima import UIMASpan 15 16from wuenlp_tools.pipeline import PipelineStep, PipelineProcessor 17from wuenlp_tools.utils.scenes import SCENE_TYPES, NONSCENE_TYPES 18 19 20def _sentence_within_nonscene(sentence): 21 overlapping_scenes = sentence.overlapping(UIMAScene) 22 for scene in overlapping_scenes: 23 scene_type_lower = scene.scene_type.lower() 24 assert scene_type_lower in SCENE_TYPES, f"SceneType is {scene.scene_type}" 25 assert NONSCENE_TYPES.issubset( 26 SCENE_TYPES), f"Nonscene types {NONSCENE_TYPES - SCENE_TYPES} not in valid scene types" 27 if scene_type_lower in NONSCENE_TYPES: 28 return True 29 30 31def visualise_document(doc: UIMADocument, title: Optional[str] = None, 32 tolerance: int = 3): 33 if title is None and doc.path is not None: 34 title = doc.path.name 35 36 document_annotation = doc.document_annotation 37 38 def _get_scene_index(scene): 39 try: 40 return scene.covered(UIMASentence)[0].position_within(document_annotation) 41 except IndexError: 42 return None 43 44 gold_borders = [_get_scene_index(scene) for scene in doc.scenes] 45 gold_borders = [border for border in gold_borders if border is not None] 46 pred_borders = [_get_scene_index(scene) for scene in doc.system_scenes] 47 pred_borders = [border for border in pred_borders if border is not None] 48 sentences = doc.sentences 49 end = len(sentences) 50 51 fps, fns, tps = 0, 0, 0 52 53 fig = go.Figure(layout_xaxis_range=[-1, end + 1], layout_yaxis_range=[-1, 2]) 54 55 wrong_linebreak = "\n" 56 57 text = [ 58 f"{textwrap.fill(sentence.text, 50).replace(wrong_linebreak, '<br>')} <br> " for i, sentence in 59 enumerate(sentences) 60 ] 61 62 x_scene = [x for x in range(end) if not _sentence_within_nonscene(sentences[x])] 63 x_nonscene = [x for x in range(end) if _sentence_within_nonscene(sentences[x])] 64 65 for y in (0, 1): 66 fig.add_trace( 67 go.Scatter(x=x_scene, y=[y] * len(x_scene), text=[t for i, t in enumerate(text) if i in x_scene], 68 mode="markers", 69 marker=dict(symbol=0, color="blue", size=5, opacity=1))) 70 fig.add_trace( 71 go.Scatter(x=x_nonscene, y=[y] * len(x_nonscene), text=[t for i, t in enumerate(text) if i in x_nonscene], 72 mode="markers", 73 marker=dict(symbol=0, color="yellow", size=5, opacity=1))) 74 75 for y, borders in enumerate((gold_borders, pred_borders)): 76 fig.add_shape( 77 type="line", 78 x0=min(borders), 79 y0=y, 80 x1=end, 81 y1=y, 82 line=dict( 83 color="black" 84 ), 85 ), 86 87 matched_borders = [] 88 for border in pred_borders: 89 fig.add_shape( 90 type="line", 91 x0=border, 92 y0=0.9, 93 x1=border, 94 y1=1.1, 95 line=dict( 96 color="black" 97 ), 98 ) 99 # find closest gold border 100 closest = min(gold_borders, key=lambda x: abs(x - border)) 101 fig.add_shape( 102 type="line", 103 x0=border, 104 y0=1, 105 x1=closest, 106 y1=0, 107 line=dict( 108 color="red" if abs(border - closest) > tolerance else "green" 109 ), 110 ) 111 if abs(border - closest) <= tolerance: 112 tps += 1 113 matched_borders.append(closest) 114 fig.add_shape( 115 type="line", 116 x0=border, 117 y0=0.9, 118 x1=border, 119 y1=1.1, 120 line=dict( 121 color="green" 122 ), 123 ) 124 else: 125 fps += 1 126 fig.add_shape( 127 type="line", 128 x0=border, 129 y0=0.9, 130 x1=border, 131 y1=1.1, 132 line=dict( 133 color="red" 134 ), 135 ) 136 137 for border in gold_borders: 138 fig.add_shape( 139 type="line", 140 x0=border, 141 y0=-0.1, 142 x1=border, 143 y1=0.1, 144 line=dict( 145 color="black" 146 ), 147 ) 148 149 if border not in matched_borders: 150 fns += 1 151 fig.add_shape( 152 type="line", 153 x0=border, 154 y0=-0.1, 155 x1=border, 156 y1=0.1, 157 line=dict( 158 color="red" 159 ) 160 ) 161 162 fig.update_layout( 163 title=f"{title}", 164 xaxis_title="Sentence Offset", 165 height=300, 166 width=1000, 167 showlegend=False, 168 yaxis=dict( 169 range=[-0.2, 1.2] # Set y-axis to range from 0 to 60 170 ) 171 ) 172 173 labelalias = {0: "Gold", 1: "Predicted", -1: "", -0.5: "", 0.5: "", 1.5: "", 2: ""} 174 175 fig.update_yaxes(fixedrange=True, labelalias=labelalias) 176 177 fig.show() 178 179 print(f"TP: {tps}, FP: {fps}, FN: {fns}") 180 return doc 181 182 183class SceneVisualiseProcessor(PipelineProcessor): 184 185 def __call__(self, doc: UIMADocument, unit_type: Type[UIMASpan] | None, overwrite: bool = False, 186 **kwargs) -> UIMADocument: 187 return visualise_document(doc) 188 189 190SceneVisualiser = PipelineStep( 191 name="SceneVisualiser", 192 processor=SceneVisualiseProcessor(), 193 unit_type=UIMAScene, 194)
def
visualise_document( doc: wuenlp.impl.uima.UIMANLPStructs.UIMADocument, title: Optional[str] = None, tolerance: int = 3):
32def visualise_document(doc: UIMADocument, title: Optional[str] = None, 33 tolerance: int = 3): 34 if title is None and doc.path is not None: 35 title = doc.path.name 36 37 document_annotation = doc.document_annotation 38 39 def _get_scene_index(scene): 40 try: 41 return scene.covered(UIMASentence)[0].position_within(document_annotation) 42 except IndexError: 43 return None 44 45 gold_borders = [_get_scene_index(scene) for scene in doc.scenes] 46 gold_borders = [border for border in gold_borders if border is not None] 47 pred_borders = [_get_scene_index(scene) for scene in doc.system_scenes] 48 pred_borders = [border for border in pred_borders if border is not None] 49 sentences = doc.sentences 50 end = len(sentences) 51 52 fps, fns, tps = 0, 0, 0 53 54 fig = go.Figure(layout_xaxis_range=[-1, end + 1], layout_yaxis_range=[-1, 2]) 55 56 wrong_linebreak = "\n" 57 58 text = [ 59 f"{textwrap.fill(sentence.text, 50).replace(wrong_linebreak, '<br>')} <br> " for i, sentence in 60 enumerate(sentences) 61 ] 62 63 x_scene = [x for x in range(end) if not _sentence_within_nonscene(sentences[x])] 64 x_nonscene = [x for x in range(end) if _sentence_within_nonscene(sentences[x])] 65 66 for y in (0, 1): 67 fig.add_trace( 68 go.Scatter(x=x_scene, y=[y] * len(x_scene), text=[t for i, t in enumerate(text) if i in x_scene], 69 mode="markers", 70 marker=dict(symbol=0, color="blue", size=5, opacity=1))) 71 fig.add_trace( 72 go.Scatter(x=x_nonscene, y=[y] * len(x_nonscene), text=[t for i, t in enumerate(text) if i in x_nonscene], 73 mode="markers", 74 marker=dict(symbol=0, color="yellow", size=5, opacity=1))) 75 76 for y, borders in enumerate((gold_borders, pred_borders)): 77 fig.add_shape( 78 type="line", 79 x0=min(borders), 80 y0=y, 81 x1=end, 82 y1=y, 83 line=dict( 84 color="black" 85 ), 86 ), 87 88 matched_borders = [] 89 for border in pred_borders: 90 fig.add_shape( 91 type="line", 92 x0=border, 93 y0=0.9, 94 x1=border, 95 y1=1.1, 96 line=dict( 97 color="black" 98 ), 99 ) 100 # find closest gold border 101 closest = min(gold_borders, key=lambda x: abs(x - border)) 102 fig.add_shape( 103 type="line", 104 x0=border, 105 y0=1, 106 x1=closest, 107 y1=0, 108 line=dict( 109 color="red" if abs(border - closest) > tolerance else "green" 110 ), 111 ) 112 if abs(border - closest) <= tolerance: 113 tps += 1 114 matched_borders.append(closest) 115 fig.add_shape( 116 type="line", 117 x0=border, 118 y0=0.9, 119 x1=border, 120 y1=1.1, 121 line=dict( 122 color="green" 123 ), 124 ) 125 else: 126 fps += 1 127 fig.add_shape( 128 type="line", 129 x0=border, 130 y0=0.9, 131 x1=border, 132 y1=1.1, 133 line=dict( 134 color="red" 135 ), 136 ) 137 138 for border in gold_borders: 139 fig.add_shape( 140 type="line", 141 x0=border, 142 y0=-0.1, 143 x1=border, 144 y1=0.1, 145 line=dict( 146 color="black" 147 ), 148 ) 149 150 if border not in matched_borders: 151 fns += 1 152 fig.add_shape( 153 type="line", 154 x0=border, 155 y0=-0.1, 156 x1=border, 157 y1=0.1, 158 line=dict( 159 color="red" 160 ) 161 ) 162 163 fig.update_layout( 164 title=f"{title}", 165 xaxis_title="Sentence Offset", 166 height=300, 167 width=1000, 168 showlegend=False, 169 yaxis=dict( 170 range=[-0.2, 1.2] # Set y-axis to range from 0 to 60 171 ) 172 ) 173 174 labelalias = {0: "Gold", 1: "Predicted", -1: "", -0.5: "", 0.5: "", 1.5: "", 2: ""} 175 176 fig.update_yaxes(fixedrange=True, labelalias=labelalias) 177 178 fig.show() 179 180 print(f"TP: {tps}, FP: {fps}, FN: {fns}") 181 return doc
class
SceneVisualiseProcessor(wuenlp_tools.pipeline.AbstractPipelineProcessor[wuenlp.impl.uima.UIMANLPStructs.UIMADocument]):
184class SceneVisualiseProcessor(PipelineProcessor): 185 186 def __call__(self, doc: UIMADocument, unit_type: Type[UIMASpan] | None, overwrite: bool = False, 187 **kwargs) -> UIMADocument: 188 return visualise_document(doc)
Base class for protocol classes.
Protocol classes are defined as::
class Proto(Protocol):
def meth(self) -> int:
...
Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing).
For example::
class C:
def meth(self) -> int:
return 0
def func(x: Proto) -> int:
return x.meth()
func(C()) # Passes static type check
See PEP 544 for details. Protocol classes decorated with @typing.runtime_checkable act as simple-minded runtime protocols that check only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as::
class GenProto[T](Protocol):
def meth(self) -> T:
...
SceneVisualiser =
PipelineStep('SceneVisualiser', processor=SceneVisualiseProcessor)
Pipeline step SceneVisualiser (SceneVisualiseProcessor).
unit type UIMAScene.