MatthiasC commited on
Commit
6b6c0db
1 Parent(s): 3ae9358

Allow free input and some fixes

Browse files
Files changed (3) hide show
  1. app.py +185 -149
  2. custom_renderer.py +4 -125
  3. requirements.txt +3 -2
app.py CHANGED
@@ -1,5 +1,5 @@
1
  import random
2
- from typing import AnyStr
3
  # import tensorflow_hub as hub
4
 
5
  import itertools
@@ -42,8 +42,8 @@ def get_sentence_embedding_model():
42
 
43
  @st.experimental_singleton
44
  def get_spacy():
45
- #nlp = spacy.load('en_core_web_lg')
46
- nlp = en_core_web_sm.load()
47
  return nlp
48
 
49
 
@@ -62,6 +62,15 @@ def get_transformer_pipeline():
62
  return pipeline("ner", model=model, tokenizer=tokenizer, grouped_entities=True)
63
 
64
 
 
 
 
 
 
 
 
 
 
65
  # Page setup
66
  st.set_page_config(
67
  page_title="Post-processing summarization fact checker",
@@ -81,10 +90,14 @@ def list_all_article_names() -> list:
81
  for file in sorted(os.listdir('./sample-articles/')):
82
  if file.endswith('.txt'):
83
  filenames.append(file.replace('.txt', ''))
 
 
84
  return filenames
85
 
86
 
87
  def fetch_article_contents(filename: str) -> AnyStr:
 
 
88
  with open(f'./sample-articles/{filename.lower()}.txt', 'r') as f:
89
  data = f.read()
90
  return data
@@ -110,13 +123,12 @@ def fetch_dependency_specific_contents(filename: str) -> AnyStr:
110
 
111
  def fetch_dependency_svg(filename: str) -> AnyStr:
112
  with open(f'./dependency-images/{filename.lower()}.txt', 'r') as f:
113
- #data = f.read()
114
- lines=[line.rstrip() for line in f]
115
  return lines
116
 
117
 
118
- def display_summary(article_name: str):
119
- summary_content = fetch_summary_contents(article_name)
120
  st.session_state.summary_output = summary_content
121
  soup = BeautifulSoup(summary_content, features="html.parser")
122
  HTML_WRAPPER = """<div style="overflow-x: auto; border: 1px solid #e6e9ef; border-radius: 0.25rem; padding: 1rem; margin-bottom: 2.5rem">{}</div>"""
@@ -161,13 +173,15 @@ def get_all_entities(text):
161
 
162
 
163
  # TODO: this functionality can be cached (e.g. by storing html file output) if wanted (or just store list of entities idk)
164
- def get_and_compare_entities(article_name: str):
165
- article_content = fetch_article_contents(article_name)
 
166
  all_entities_per_sentence = get_all_entities_per_sentence(article_content)
167
  # st.session_state.entities_per_sentence_article = all_entities_per_sentence
168
  entities_article = list(itertools.chain.from_iterable(all_entities_per_sentence))
169
 
170
- summary_content = fetch_summary_contents(article_name)
 
171
  all_entities_per_sentence = get_all_entities_per_sentence(summary_content)
172
  # st.session_state.entities_per_sentence_summary = all_entities_per_sentence
173
  entities_summary = list(itertools.chain.from_iterable(all_entities_per_sentence))
@@ -179,7 +193,7 @@ def get_and_compare_entities(article_name: str):
179
  if any(entity.lower() in substring_entity.lower() for substring_entity in entities_article):
180
  matched_entities.append(entity)
181
  elif any(
182
- np.inner(sentence_embedding_model.encode(entity), sentence_embedding_model.encode(art_entity)) > 0.9 for
183
  art_entity in entities_article):
184
  matched_entities.append(entity)
185
  else:
@@ -187,14 +201,14 @@ def get_and_compare_entities(article_name: str):
187
  return matched_entities, unmatched_entities
188
 
189
 
190
- def highlight_entities(article_name: str):
191
- summary_content = fetch_summary_contents(article_name)
192
-
193
  markdown_start_red = "<mark class=\"entity\" style=\"background: rgb(238, 135, 135);\">"
194
  markdown_start_green = "<mark class=\"entity\" style=\"background: rgb(121, 236, 121);\">"
195
  markdown_end = "</mark>"
196
 
197
- matched_entities, unmatched_entities = get_and_compare_entities(article_name)
198
 
199
  for entity in matched_entities:
200
  summary_content = summary_content.replace(entity, markdown_start_green + entity + markdown_end)
@@ -209,10 +223,9 @@ def highlight_entities(article_name: str):
209
  return HTML_WRAPPER.format(soup)
210
 
211
 
212
- def render_dependency_parsing(text: str):
213
- html = render_sentence_custom(text)
214
  html = html.replace("\n\n", "\n")
215
- print(get_svg(html))
216
  st.write(get_svg(html), unsafe_allow_html=True)
217
 
218
 
@@ -237,7 +250,6 @@ def check_dependency(article: bool):
237
  start_id = sentence.start
238
  end_id = sentence.end
239
  for t in tok_l:
240
- # print(t)
241
  if t["id"] < start_id or t["id"] > end_id:
242
  continue
243
  head = tok_l[t['head']]
@@ -261,7 +273,6 @@ def check_dependency(article: bool):
261
  "identifier": identifier, "sentence": str(sentence)})
262
  else:
263
  continue
264
- # print(f'NOW TEST LIST DICT: {test_list_dict_output}')
265
  return test_list_dict_output
266
  # return all_deps
267
 
@@ -273,6 +284,38 @@ def is_valid_url(url: str) -> bool:
273
  return True
274
 
275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  # Start session
277
  if 'results' not in st.session_state:
278
  st.session_state.results = []
@@ -298,12 +341,11 @@ metric, indicating the trustworthiness of the generated summary. Throughout this
298
  results for some methods on specific examples. These text blocks will be indicated and they change according to the
299
  currently selected article.""")
300
 
301
-
302
  sentence_embedding_model = get_sentence_embedding_model()
303
  # tagger = get_flair_tagger()
304
  ner_model = get_transformer_pipeline()
305
  nlp = get_spacy()
306
- #nlp = en_core_web_sm.load()
307
 
308
  # GENERATING SUMMARIES PART
309
  st.header("Generating summaries")
@@ -321,131 +363,125 @@ article_text = st.text_area(
321
  height=150
322
  )
323
 
324
- st.markdown("Below you can find the generated summary for the article. Based on empirical research, we will discuss "
325
- "two main methods that detect some common errors. We can then score different summaries, to indicate how "
326
- "factual a summary is for a given article. The idea is that in production, you could generate a set of "
327
- "summaries for the same article, with different parameters (or even different models). By using "
328
- "post-processing error detection, we can then select the best possible summary.")
329
- if st.session_state.article_text:
330
- with st.spinner('Generating summary...'):
331
- # classify_comment(article_text, selected_model)
332
-
333
- summary_displayed = display_summary(selected_article)
334
-
335
- st.write("**Generated summary:**", summary_displayed, unsafe_allow_html=True)
336
- else:
337
- st.error('**Error**: No comment to classify. Please provide a comment.',
338
- help="Generate summary for the given article text")
339
-
340
-
341
- def render_svg(svg_file):
342
- with open(svg_file, "r") as f:
343
- lines = f.readlines()
344
- svg = "".join(lines)
345
-
346
- # """Renders the given svg string."""
347
- b64 = base64.b64encode(svg.encode("utf-8")).decode("utf-8")
348
- html = r'<img src="data:image/svg+xml;base64,%s"/>' % b64
349
- return html
350
-
351
-
352
- # ENTITY MATCHING PART
353
- st.header("Entity matching")
354
- st.markdown("The first method we will discuss is called **Named Entity Recognition** (NER). NER is the task of "
355
- "identifying and categorising key information (entities) in text. An entity can be a singular word or a "
356
- "series of words that consistently refers to the same thing. Common entity classes are person names, "
357
- "organisations, locations and so on. By applying NER to both the article and its summary, we can spot "
358
- "possible **hallucinations**. Hallucinations are words generated by the model that are not supported by "
359
- "the source input. In theory all entities in the summary (such as dates, locations and so on), "
360
- "should also be present in the article. Thus we can extract all entities from the summary and compare "
361
- "them to the entities of the original article, spotting potential hallucinations. The more unmatched "
362
- "entities we find, the lower the factualness score of the summary. ")
363
- with st.spinner("Calculating and matching entities..."):
364
- entity_match_html = highlight_entities(selected_article)
365
- st.write(entity_match_html, unsafe_allow_html=True)
366
- red_text = """<font color="black"><span style="background-color: rgb(238, 135, 135); opacity:
367
- 1;">red</span></font> """
368
- green_text = """<font color="black">
369
- <span style="background-color: rgb(121, 236, 121); opacity: 1;">green</span>
370
- </font>"""
371
-
372
- markdown_start_red = "<mark class=\"entity\" style=\"background: rgb(238, 135, 135);\">"
373
- markdown_start_green = "<mark class=\"entity\" style=\"background: rgb(121, 236, 121);\">"
374
- st.markdown("We call this technique “entity matching” and here you can see what this looks like when we apply "
375
- "this method on the summary. Entities in the summary are marked " + green_text + " when the entity "
376
- "also exists in the "
377
- "article, "
378
- "while unmatched "
379
- "entities are "
380
- "marked " +
381
- red_text + ". Several of the example articles and their summaries indicate different errors we find "
382
- "by using this technique. Based on which article you choose, we provide a short "
383
- "explanation of the results below.",
384
- unsafe_allow_html=True)
385
- entity_specific_text = fetch_entity_specific_contents(selected_article)
386
- soup = BeautifulSoup(entity_specific_text, features="html.parser")
387
- HTML_WRAPPER = """<div style="overflow-x: auto; border: 1px solid #e6e9ef; border-radius: 0.25rem; padding: 1rem;
388
- margin-bottom: 2.5rem">{}</div> """
389
- st.write("💡👇 **Specific example explanation** 👇💡", HTML_WRAPPER.format(soup), unsafe_allow_html=True)
390
-
391
- # DEPENDENCY PARSING PART
392
- st.header("Dependency comparison")
393
- st.markdown("The second method we use for post-processing is called **Dependency parsing**: the process in which the "
394
- "grammatical structure in a sentence is analysed, to find out related words as well as the type of the "
395
- "relationship between them. For the sentence “Jan’s wife is called Sarah” you would get the following "
396
- "dependency graph:")
397
-
398
- # TODO: I wonder why the first doesn't work but the second does (it doesn't show deps otherwise)
399
- # st.image("ExampleParsing.svg")
400
- st.write(render_svg('ExampleParsing.svg'), unsafe_allow_html=True)
401
- st.markdown("Here, “Jan” is the “poss” (possession modifier) of “wife”. If suddenly the summary would read “Jan’s "
402
- "husband…”, there would be a dependency in the summary that is non-existent in the article itself (namely "
403
- "“Jan” is the “poss” of “husband”). However, often new dependencies are introduced in the summary that "
404
- "are still correct. “The borders of Ukraine” have a different dependency between “borders” and “Ukraine” "
405
- "than “Ukraine’s borders”, while both descriptions have the same meaning. So just matching all "
406
- "dependencies between article and summary (as we did with entity matching) would not be a robust method.")
407
- st.markdown("However, by empirical testing, we have found that there are certain dependencies which can be used for "
408
- "such matching techniques. When unmatched, these specific dependencies are often an indication of a "
409
- "wrongly constructed sentence. **Should I explain this more/better or is it enough that I explain by "
410
- "example specific run throughs?**. We found 2(/3 TODO) common dependencies which, when present in the "
411
- "summary but not in the article, are highly indicative of factualness errors. Furthermore, we only check "
412
- "dependencies between an existing **entity** and its direct connections. Below we highlight all unmatched "
413
- "dependencies that satisfy the discussed constraints. We also discuss the specific results for the "
414
- "currently selected article.")
415
- with st.spinner("Doing dependency parsing..."):
416
- # TODO RIGHT IF FUNCTION (IF EXAMPLE AND IF INPUT UNCHANGED)
417
- #if selected_article == 'article11':
418
- if True:
419
- for cur_svg_image in fetch_dependency_svg(selected_article):
420
- st.write(cur_svg_image, unsafe_allow_html=True)
421
  else:
422
- summary_deps = check_dependency(False)
423
- article_deps = check_dependency(True)
424
- total_unmatched_deps = []
425
- for summ_dep in summary_deps:
426
- if not any(summ_dep['identifier'] in art_dep['identifier'] for art_dep in article_deps):
427
- total_unmatched_deps.append(summ_dep)
428
- # print(f'ALL UNMATCHED DEPS ARE: {total_unmatched_deps}')
429
- # render_dependency_parsing(check_dependency(False))
430
- if total_unmatched_deps:
431
- for current_drawing_list in total_unmatched_deps:
432
- render_dependency_parsing(current_drawing_list)
433
- dep_specific_text = fetch_dependency_specific_contents(selected_article)
434
- soup = BeautifulSoup(dep_specific_text, features="html.parser")
435
- HTML_WRAPPER = """<div style="overflow-x: auto; border: 1px solid #e6e9ef; border-radius: 0.25rem; padding: 1rem;
436
- margin-bottom: 2.5rem">{}</div> """
437
- st.write("💡👇 **Specific example explanation** 👇💡", HTML_WRAPPER.format(soup), unsafe_allow_html=True)
438
-
439
- # OUTRO/CONCLUSION
440
- st.header("Wrapping up")
441
- st.markdown("We have presented 2 methods that try to improve summaries via post-processing steps. Entity matching can "
442
- "be used to solve hallucinations, while dependency comparison can be used to filter out some bad "
443
- "sentences (and thus worse summaries). These methods highlight the possibilities of post-processing "
444
- "AI-made summaries, but are only a basic introduction. As the methods were empirically tested they are "
445
- "definitely not sufficiently robust for general use-cases. (something about that we tested also RE and "
446
- "maybe other things).")
447
- st.markdown("####")
448
- st.markdown("Below we generated 5 different kind of summaries from the article in which their ranks are estimated, "
449
- "and hopefully the best summary (read: the one that a human would prefer or indicate as the best one) "
450
- "will be at the top. TODO: implement this (at the end I think) and also put something in the text with "
451
- "the actual parameters or something? ")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import random
2
+ from typing import AnyStr, List, Dict
3
  # import tensorflow_hub as hub
4
 
5
  import itertools
 
42
 
43
  @st.experimental_singleton
44
  def get_spacy():
45
+ # nlp = spacy.load('en_core_web_lg')
46
+ nlp = en_core_web_lg.load()
47
  return nlp
48
 
49
 
 
62
  return pipeline("ner", model=model, tokenizer=tokenizer, grouped_entities=True)
63
 
64
 
65
+ @st.experimental_singleton
66
+ def get_summarizer_model():
67
+ model_name = 'google/pegasus-cnn_dailymail'
68
+ summarizer_model = pipeline("summarization", model=model_name, tokenizer=model_name,
69
+ device=0 if torch.cuda.is_available() else -1)
70
+
71
+ return summarizer_model
72
+
73
+
74
  # Page setup
75
  st.set_page_config(
76
  page_title="Post-processing summarization fact checker",
 
90
  for file in sorted(os.listdir('./sample-articles/')):
91
  if file.endswith('.txt'):
92
  filenames.append(file.replace('.txt', ''))
93
+ # Append free use possibility:
94
+ filenames.append("Provide your own input")
95
  return filenames
96
 
97
 
98
  def fetch_article_contents(filename: str) -> AnyStr:
99
+ if (filename == "Provide your own input"):
100
+ return " "
101
  with open(f'./sample-articles/{filename.lower()}.txt', 'r') as f:
102
  data = f.read()
103
  return data
 
123
 
124
  def fetch_dependency_svg(filename: str) -> AnyStr:
125
  with open(f'./dependency-images/{filename.lower()}.txt', 'r') as f:
126
+ # data = f.read()
127
+ lines = [line.rstrip() for line in f]
128
  return lines
129
 
130
 
131
+ def display_summary(summary_content: str):
 
132
  st.session_state.summary_output = summary_content
133
  soup = BeautifulSoup(summary_content, features="html.parser")
134
  HTML_WRAPPER = """<div style="overflow-x: auto; border: 1px solid #e6e9ef; border-radius: 0.25rem; padding: 1rem; margin-bottom: 2.5rem">{}</div>"""
 
173
 
174
 
175
  # TODO: this functionality can be cached (e.g. by storing html file output) if wanted (or just store list of entities idk)
176
+ def get_and_compare_entities():
177
+ #article_content = fetch_article_contents(article_name)
178
+ article_content = st.session_state.article_text
179
  all_entities_per_sentence = get_all_entities_per_sentence(article_content)
180
  # st.session_state.entities_per_sentence_article = all_entities_per_sentence
181
  entities_article = list(itertools.chain.from_iterable(all_entities_per_sentence))
182
 
183
+ #summary_content = fetch_summary_contents(article_name)
184
+ summary_content = st.session_state.summary_output
185
  all_entities_per_sentence = get_all_entities_per_sentence(summary_content)
186
  # st.session_state.entities_per_sentence_summary = all_entities_per_sentence
187
  entities_summary = list(itertools.chain.from_iterable(all_entities_per_sentence))
 
193
  if any(entity.lower() in substring_entity.lower() for substring_entity in entities_article):
194
  matched_entities.append(entity)
195
  elif any(
196
+ np.inner(sentence_embedding_model.encode(entity, show_progress_bar=False), sentence_embedding_model.encode(art_entity, show_progress_bar=False)) > 0.9 for
197
  art_entity in entities_article):
198
  matched_entities.append(entity)
199
  else:
 
201
  return matched_entities, unmatched_entities
202
 
203
 
204
+ def highlight_entities():
205
+ #summary_content = fetch_summary_contents(article_name)
206
+ summary_content = st.session_state.summary_output
207
  markdown_start_red = "<mark class=\"entity\" style=\"background: rgb(238, 135, 135);\">"
208
  markdown_start_green = "<mark class=\"entity\" style=\"background: rgb(121, 236, 121);\">"
209
  markdown_end = "</mark>"
210
 
211
+ matched_entities, unmatched_entities = get_and_compare_entities()
212
 
213
  for entity in matched_entities:
214
  summary_content = summary_content.replace(entity, markdown_start_green + entity + markdown_end)
 
223
  return HTML_WRAPPER.format(soup)
224
 
225
 
226
+ def render_dependency_parsing(text: Dict):
227
+ html = render_sentence_custom(text, nlp)
228
  html = html.replace("\n\n", "\n")
 
229
  st.write(get_svg(html), unsafe_allow_html=True)
230
 
231
 
 
250
  start_id = sentence.start
251
  end_id = sentence.end
252
  for t in tok_l:
 
253
  if t["id"] < start_id or t["id"] > end_id:
254
  continue
255
  head = tok_l[t['head']]
 
273
  "identifier": identifier, "sentence": str(sentence)})
274
  else:
275
  continue
 
276
  return test_list_dict_output
277
  # return all_deps
278
 
 
284
  return True
285
 
286
 
287
+ def render_svg(svg_file):
288
+ with open(svg_file, "r") as f:
289
+ lines = f.readlines()
290
+ svg = "".join(lines)
291
+
292
+ # """Renders the given svg string."""
293
+ b64 = base64.b64encode(svg.encode("utf-8")).decode("utf-8")
294
+ html = r'<img src="data:image/svg+xml;base64,%s"/>' % b64
295
+ return html
296
+
297
+
298
+ def generate_abstractive_summary(text, type, min_len=120, max_len=512, **kwargs):
299
+ summarization_model = get_summarizer_model()
300
+ text = text.strip().replace("\n", " ")
301
+ if type == "top_p":
302
+ text = summarization_model(text, min_length=min_len,
303
+ max_length=max_len,
304
+ top_k=50, top_p=0.95, clean_up_tokenization_spaces=True)
305
+ elif type == "greedy":
306
+ text = summarization_model(text, min_length=min_len,
307
+ max_length=max_len, clean_up_tokenization_spaces=True)
308
+ elif type == "top_k":
309
+ text = summarization_model(text, min_length=min_len, max_length=max_len, top_k=50,
310
+ clean_up_tokenization_spaces=True)
311
+ elif type == "beam":
312
+ text = summarization_model(text, min_length=min_len,
313
+ max_length=max_len,
314
+ clean_up_tokenization_spaces=True, **kwargs)
315
+ summary = text[0]['summary_text'].replace("<n>", " ")
316
+ return summary
317
+
318
+
319
  # Start session
320
  if 'results' not in st.session_state:
321
  st.session_state.results = []
 
341
  results for some methods on specific examples. These text blocks will be indicated and they change according to the
342
  currently selected article.""")
343
 
 
344
  sentence_embedding_model = get_sentence_embedding_model()
345
  # tagger = get_flair_tagger()
346
  ner_model = get_transformer_pipeline()
347
  nlp = get_spacy()
348
+ # nlp = en_core_web_sm.load()
349
 
350
  # GENERATING SUMMARIES PART
351
  st.header("Generating summaries")
 
363
  height=150
364
  )
365
 
366
+ summarize_button = st.button(label='Process article content', help="Generates summary and applies entity matching and dependency parsing for given article")
367
+
368
+ if summarize_button:
369
+ st.session_state.article_text = article_text
370
+ st.markdown("Below you can find the generated summary for the article. Based on empirical research, we will discuss "
371
+ "two main methods that detect some common errors. We can then score different summaries, to indicate how "
372
+ "factual a summary is for a given article. The idea is that in production, you could generate a set of "
373
+ "summaries for the same article, with different parameters (or even different models). By using "
374
+ "post-processing error detection, we can then select the best possible summary.")
375
+ if st.session_state.article_text:
376
+ with st.spinner('Generating summary...'):
377
+ # classify_comment(article_text, selected_model)
378
+ if selected_article != "Provide your own input" and article_text == fetch_article_contents(selected_article):
379
+ st.session_state.unchanged_text = True
380
+ summary_content = fetch_summary_contents(selected_article)
381
+ else:
382
+ summary_content = generate_abstractive_summary(article_text, type="beam", do_sample=True, num_beams=15, no_repeat_ngram_size=4)
383
+ st.session_state.unchanged_text = False
384
+ summary_displayed = display_summary(summary_content)
385
+ st.write("**Generated summary:**", summary_displayed, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  else:
387
+ st.error('**Error**: No comment to classify. Please provide a comment.')
388
+
389
+ # ENTITY MATCHING PART
390
+ st.header("Entity matching")
391
+ st.markdown("The first method we will discuss is called **Named Entity Recognition** (NER). NER is the task of "
392
+ "identifying and categorising key information (entities) in text. An entity can be a singular word or a "
393
+ "series of words that consistently refers to the same thing. Common entity classes are person names, "
394
+ "organisations, locations and so on. By applying NER to both the article and its summary, we can spot "
395
+ "possible **hallucinations**. Hallucinations are words generated by the model that are not supported by "
396
+ "the source input. In theory all entities in the summary (such as dates, locations and so on), "
397
+ "should also be present in the article. Thus we can extract all entities from the summary and compare "
398
+ "them to the entities of the original article, spotting potential hallucinations. The more unmatched "
399
+ "entities we find, the lower the factualness score of the summary. ")
400
+ with st.spinner("Calculating and matching entities..."):
401
+ entity_match_html = highlight_entities()
402
+ st.write(entity_match_html, unsafe_allow_html=True)
403
+ red_text = """<font color="black"><span style="background-color: rgb(238, 135, 135); opacity:
404
+ 1;">red</span></font> """
405
+ green_text = """<font color="black">
406
+ <span style="background-color: rgb(121, 236, 121); opacity: 1;">green</span>
407
+ </font>"""
408
+
409
+ markdown_start_red = "<mark class=\"entity\" style=\"background: rgb(238, 135, 135);\">"
410
+ markdown_start_green = "<mark class=\"entity\" style=\"background: rgb(121, 236, 121);\">"
411
+ st.markdown("We call this technique “entity matching” and here you can see what this looks like when we apply "
412
+ "this method on the summary. Entities in the summary are marked " + green_text + " when the entity "
413
+ "also exists in the "
414
+ "article, "
415
+ "while unmatched "
416
+ "entities are "
417
+ "marked " +
418
+ red_text + ". Several of the example articles and their summaries indicate different errors we find "
419
+ "by using this technique. Based on which article you choose, we provide a short "
420
+ "explanation of the results below.",
421
+ unsafe_allow_html=True)
422
+ if st.session_state.unchanged_text:
423
+ entity_specific_text = fetch_entity_specific_contents(selected_article)
424
+ soup = BeautifulSoup(entity_specific_text, features="html.parser")
425
+ HTML_WRAPPER = """<div style="overflow-x: auto; border: 1px solid #e6e9ef; border-radius: 0.25rem; padding: 1rem;
426
+ margin-bottom: 2.5rem">{}</div> """
427
+ st.write("💡👇 **Specific example explanation** 👇💡", HTML_WRAPPER.format(soup), unsafe_allow_html=True)
428
+
429
+ # DEPENDENCY PARSING PART
430
+ st.header("Dependency comparison")
431
+ st.markdown("The second method we use for post-processing is called **Dependency parsing**: the process in which the "
432
+ "grammatical structure in a sentence is analysed, to find out related words as well as the type of the "
433
+ "relationship between them. For the sentence “Jan’s wife is called Sarah” you would get the following "
434
+ "dependency graph:")
435
+
436
+ # TODO: I wonder why the first doesn't work but the second does (it doesn't show deps otherwise)
437
+ # st.image("ExampleParsing.svg")
438
+ st.write(render_svg('ExampleParsing.svg'), unsafe_allow_html=True)
439
+ st.markdown("Here, “Jan” is the “poss” (possession modifier) of “wife”. If suddenly the summary would read “Jan’s "
440
+ "husband…”, there would be a dependency in the summary that is non-existent in the article itself (namely "
441
+ "“Jan” is the “poss” of “husband”). However, often new dependencies are introduced in the summary that "
442
+ "are still correct. “The borders of Ukraine” have a different dependency between “borders” and “Ukraine” "
443
+ "than “Ukraine’s borders”, while both descriptions have the same meaning. So just matching all "
444
+ "dependencies between article and summary (as we did with entity matching) would not be a robust method.")
445
+ st.markdown("However, by empirical testing, we have found that there are certain dependencies which can be used for "
446
+ "such matching techniques. When unmatched, these specific dependencies are often an indication of a "
447
+ "wrongly constructed sentence. **Should I explain this more/better or is it enough that I explain by "
448
+ "example specific run throughs?**. We found 2(/3 TODO) common dependencies which, when present in the "
449
+ "summary but not in the article, are highly indicative of factualness errors. Furthermore, we only check "
450
+ "dependencies between an existing **entity** and its direct connections. Below we highlight all unmatched "
451
+ "dependencies that satisfy the discussed constraints. We also discuss the specific results for the "
452
+ "currently selected article.")
453
+ with st.spinner("Doing dependency parsing..."):
454
+ # TODO RIGHT IF FUNCTION (IF EXAMPLE AND IF INPUT UNCHANGED)
455
+ # if selected_article == 'article11':
456
+ if st.session_state.unchanged_text:
457
+ for cur_svg_image in fetch_dependency_svg(selected_article):
458
+ st.write(cur_svg_image, unsafe_allow_html=True)
459
+ dep_specific_text = fetch_dependency_specific_contents(selected_article)
460
+ soup = BeautifulSoup(dep_specific_text, features="html.parser")
461
+ HTML_WRAPPER = """<div style="overflow-x: auto; border: 1px solid #e6e9ef; border-radius: 0.25rem; padding: 1rem;
462
+ margin-bottom: 2.5rem">{}</div> """
463
+ st.write("💡👇 **Specific example explanation** 👇💡", HTML_WRAPPER.format(soup), unsafe_allow_html=True)
464
+ else:
465
+ summary_deps = check_dependency(False)
466
+ article_deps = check_dependency(True)
467
+ total_unmatched_deps = []
468
+ for summ_dep in summary_deps:
469
+ if not any(summ_dep['identifier'] in art_dep['identifier'] for art_dep in article_deps):
470
+ total_unmatched_deps.append(summ_dep)
471
+ if total_unmatched_deps:
472
+ for current_drawing_list in total_unmatched_deps:
473
+ render_dependency_parsing(current_drawing_list)
474
+
475
+ # OUTRO/CONCLUSION
476
+ st.header("Wrapping up")
477
+ st.markdown("We have presented 2 methods that try to improve summaries via post-processing steps. Entity matching can "
478
+ "be used to solve hallucinations, while dependency comparison can be used to filter out some bad "
479
+ "sentences (and thus worse summaries). These methods highlight the possibilities of post-processing "
480
+ "AI-made summaries, but are only a basic introduction. As the methods were empirically tested they are "
481
+ "definitely not sufficiently robust for general use-cases. (something about that we tested also RE and "
482
+ "maybe other things).")
483
+ st.markdown("####")
484
+ st.markdown("Below we generated 5 different kind of summaries from the article in which their ranks are estimated, "
485
+ "and hopefully the best summary (read: the one that a human would prefer or indicate as the best one) "
486
+ "will be at the top. TODO: implement this (at the end I think) and also put something in the text with "
487
+ "the actual parameters or something? ")
custom_renderer.py CHANGED
@@ -1,10 +1,8 @@
1
- from typing import Dict, Any
2
 
3
- import numpy as np
4
  import spacy
5
  from PIL import ImageFont
6
 
7
- from spacy.tokens import Doc
8
 
9
 
10
  def get_pil_text_size(text, font_size, font_name):
@@ -78,8 +76,7 @@ def get_arrowhead(direction: str, x: int, y: int, end: int) -> str:
78
  return f"M{p1},{y + 2} L{p2},{y - arrow_width} {p3},{y - arrow_width}"
79
 
80
 
81
- # parsed = [{'words': [{'text': 'The', 'tag': 'DET', 'lemma': None}, {'text': 'OnePlus', 'tag': 'PROPN', 'lemma': None}, {'text': '10', 'tag': 'NUM', 'lemma': None}, {'text': 'Pro', 'tag': 'PROPN', 'lemma': None}, {'text': 'is', 'tag': 'AUX', 'lemma': None}, {'text': 'the', 'tag': 'DET', 'lemma': None}, {'text': 'company', 'tag': 'NOUN', 'lemma': None}, {'text': "'s", 'tag': 'PART', 'lemma': None}, {'text': 'first', 'tag': 'ADJ', 'lemma': None}, {'text': 'flagship', 'tag': 'NOUN', 'lemma': None}, {'text': 'phone.', 'tag': 'NOUN', 'lemma': None}], 'arcs': [{'start': 0, 'end': 3, 'label': 'det', 'dir': 'left'}, {'start': 1, 'end': 3, 'label': 'nmod', 'dir': 'left'}, {'start': 1, 'end': 2, 'label': 'nummod', 'dir': 'right'}, {'start': 3, 'end': 4, 'label': 'nsubj', 'dir': 'left'}, {'start': 5, 'end': 6, 'label': 'det', 'dir': 'left'}, {'start': 6, 'end': 10, 'label': 'poss', 'dir': 'left'}, {'start': 6, 'end': 7, 'label': 'case', 'dir': 'right'}, {'start': 8, 'end': 10, 'label': 'amod', 'dir': 'left'}, {'start': 9, 'end': 10, 'label': 'compound', 'dir': 'left'}, {'start': 4, 'end': 10, 'label': 'attr', 'dir': 'right'}], 'settings': {'lang': 'en', 'direction': 'ltr'}}]
82
- def render_sentence_custom(unmatched_list: Dict):
83
  TPL_DEP_WORDS = """
84
  <text class="displacy-token" fill="currentColor" text-anchor="start" y="{y}">
85
  <tspan class="displacy-word" fill="currentColor" x="{x}">{text}</tspan>
@@ -91,64 +88,14 @@ def render_sentence_custom(unmatched_list: Dict):
91
  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:lang="{lang}" id="{id}" class="displacy" width="{width}" height="{height}" direction="{dir}" style="max-width: none; height: {height}px; color: {color}; background: {bg}; font-family: {font}; direction: {dir}">{content}</svg>
92
  """
93
  arcs_svg = []
94
- nlp = spacy.load('en_core_web_lg')
95
  doc = nlp(unmatched_list["sentence"])
96
- # words = {}
97
- # unmatched_list = [parse_deps(doc)]
98
- # #print(parsed)
99
- # for i, p in enumerate(unmatched_list):
100
- # arcs = p["arcs"]
101
- # words = p["words"]
102
- # for i, a in enumerate(arcs):
103
- # #CHECK CERTAIN DEPS (ALSO ADD/CHANGE BELOW WHEN CHANGING HERE)
104
- # if a["label"] == "amod":
105
- # couples = (a["start"], a["end"])
106
- # elif a["label"] == "pobj":
107
- # couples = (a["start"], a["end"])
108
- # #couples = (3,5)
109
- #
110
- # x_value_counter = 10
111
- # index_counter = 0
112
- # svg_words = []
113
- # coords_test = []
114
- # for i, word in enumerate(words):
115
- # word = word["text"]
116
- # word = word + " "
117
- # pixel_x_length = get_pil_text_size(word, 16, 'arial.ttf')[0]
118
- # svg_words.append(TPL_DEP_WORDS.format(text=word, tag="", x=x_value_counter, y=70))
119
- # if index_counter >= couples[0] and index_counter <= couples[1]:
120
- # coords_test.append(x_value_counter)
121
- # x_value_counter += 50
122
- # index_counter += 1
123
- # x_value_counter += pixel_x_length + 4
124
- # for i, a in enumerate(arcs):
125
- # if a["label"] == "amod":
126
- # arcs_svg.append(render_arrow(a["label"], coords_test[0], coords_test[-1], a["dir"], i))
127
- # elif a["label"] == "pobj":
128
- # arcs_svg.append(render_arrow(a["label"], coords_test[0], coords_test[-1], a["dir"], i))
129
- #
130
- # content = "".join(svg_words) + "".join(arcs_svg)
131
- #
132
- # full_svg = TPL_DEP_SVG.format(
133
- # id=0,
134
- # width=1200, #600
135
- # height=250, #125
136
- # color="#00000",
137
- # bg="#ffffff",
138
- # font="Arial",
139
- # content=content,
140
- # dir="ltr",
141
- # lang="en",
142
- # )
143
 
144
  x_value_counter = 10
145
  index_counter = 0
146
  svg_words = []
147
- words = unmatched_list["sentence"].split(" ")
148
  coords_test = []
149
- #print(unmatched_list)
150
- #print(words)
151
- #print("NOW")
152
  direction_current = "rtl"
153
  if unmatched_list["cur_word_index"] < unmatched_list["target_word_index"]:
154
  min_index = unmatched_list["cur_word_index"]
@@ -169,8 +116,6 @@ def render_sentence_custom(unmatched_list: Dict):
169
  index_counter += 1
170
  x_value_counter += pixel_x_length + 4
171
 
172
- # TODO: DYNAMIC DIRECTION MAKING (SHOULD GIVE WITH DICT I THINK)
173
- #print(coords_test)
174
  arcs_svg.append(render_arrow(unmatched_list['dep'], coords_test[0], coords_test[-1], direction_current, i))
175
 
176
  content = "".join(svg_words) + "".join(arcs_svg)
@@ -189,69 +134,3 @@ def render_sentence_custom(unmatched_list: Dict):
189
  return full_svg
190
 
191
 
192
- def parse_deps(orig_doc: Doc, options: Dict[str, Any] = {}) -> Dict[str, Any]:
193
- """Generate dependency parse in {'words': [], 'arcs': []} format.
194
-
195
- doc (Doc): Document do parse.
196
- RETURNS (dict): Generated dependency parse keyed by words and arcs.
197
- """
198
- doc = Doc(orig_doc.vocab).from_bytes(orig_doc.to_bytes(exclude=["user_data"]))
199
- if not doc.has_annotation("DEP"):
200
- print("WARNING")
201
- if options.get("collapse_phrases", False):
202
- with doc.retokenize() as retokenizer:
203
- for np in list(doc.noun_chunks):
204
- attrs = {
205
- "tag": np.root.tag_,
206
- "lemma": np.root.lemma_,
207
- "ent_type": np.root.ent_type_,
208
- }
209
- retokenizer.merge(np, attrs=attrs)
210
- if options.get("collapse_punct", True):
211
- spans = []
212
- for word in doc[:-1]:
213
- if word.is_punct or not word.nbor(1).is_punct:
214
- continue
215
- start = word.i
216
- end = word.i + 1
217
- while end < len(doc) and doc[end].is_punct:
218
- end += 1
219
- span = doc[start:end]
220
- spans.append((span, word.tag_, word.lemma_, word.ent_type_))
221
- with doc.retokenize() as retokenizer:
222
- for span, tag, lemma, ent_type in spans:
223
- attrs = {"tag": tag, "lemma": lemma, "ent_type": ent_type}
224
- retokenizer.merge(span, attrs=attrs)
225
- fine_grained = options.get("fine_grained")
226
- add_lemma = options.get("add_lemma")
227
- words = [
228
- {
229
- "text": w.text,
230
- "tag": w.tag_ if fine_grained else w.pos_,
231
- "lemma": w.lemma_ if add_lemma else None,
232
- }
233
- for w in doc
234
- ]
235
- arcs = []
236
- for word in doc:
237
- if word.i < word.head.i:
238
- arcs.append(
239
- {"start": word.i, "end": word.head.i, "label": word.dep_, "dir": "left"}
240
- )
241
- elif word.i > word.head.i:
242
- arcs.append(
243
- {
244
- "start": word.head.i,
245
- "end": word.i,
246
- "label": word.dep_,
247
- "dir": "right",
248
- }
249
- )
250
- return {"words": words, "arcs": arcs, "settings": get_doc_settings(orig_doc)}
251
-
252
-
253
- def get_doc_settings(doc: Doc) -> Dict[str, Any]:
254
- return {
255
- "lang": doc.lang_,
256
- "direction": doc.vocab.writing_system.get("direction", "ltr"),
257
- }
 
1
+ from typing import Dict
2
 
 
3
  import spacy
4
  from PIL import ImageFont
5
 
 
6
 
7
 
8
  def get_pil_text_size(text, font_size, font_name):
 
76
  return f"M{p1},{y + 2} L{p2},{y - arrow_width} {p3},{y - arrow_width}"
77
 
78
 
79
+ def render_sentence_custom(unmatched_list: Dict, nlp):
 
80
  TPL_DEP_WORDS = """
81
  <text class="displacy-token" fill="currentColor" text-anchor="start" y="{y}">
82
  <tspan class="displacy-word" fill="currentColor" x="{x}">{text}</tspan>
 
88
  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:lang="{lang}" id="{id}" class="displacy" width="{width}" height="{height}" direction="{dir}" style="max-width: none; height: {height}px; color: {color}; background: {bg}; font-family: {font}; direction: {dir}">{content}</svg>
89
  """
90
  arcs_svg = []
91
+ #nlp = spacy.load('en_core_web_lg')
92
  doc = nlp(unmatched_list["sentence"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
  x_value_counter = 10
95
  index_counter = 0
96
  svg_words = []
97
+ #words = unmatched_list["sentence"].split(" ")
98
  coords_test = []
 
 
 
99
  direction_current = "rtl"
100
  if unmatched_list["cur_word_index"] < unmatched_list["target_word_index"]:
101
  min_index = unmatched_list["cur_word_index"]
 
116
  index_counter += 1
117
  x_value_counter += pixel_x_length + 4
118
 
 
 
119
  arcs_svg.append(render_arrow(unmatched_list['dep'], coords_test[0], coords_test[-1], direction_current, i))
120
 
121
  content = "".join(svg_words) + "".join(arcs_svg)
 
134
  return full_svg
135
 
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -5,5 +5,6 @@ transformers-interpret==0.5.2
5
  sentence-transformers==2.2.0
6
  spacy==3.0.0
7
  spacy_streamlit==1.0.3
8
- en_core_web_lg @ https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.0.0/en_core_web_lg-3.0.0.tar.gz
9
- en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.0.0/en_core_web_sm-3.0.0.tar.gz
 
 
5
  sentence-transformers==2.2.0
6
  spacy==3.0.0
7
  spacy_streamlit==1.0.3
8
+ ###### en_core_web_lg @ https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.0.0/en_core_web_lg-3.0.0.tar.gz
9
+ en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.0.0/en_core_web_sm-3.0.0.tar.gz
10
+ en_core_web_lg @ https://huggingface.co/spacy/en_core_web_lg/resolve/main/en_core_web_lg-any-py3-none-any.whl