File size: 16,795 Bytes
982cda3
543c8b7
eab29b2
d43ee70
 
eab29b2
 
 
9dd6010
d43ee70
9dd6010
 
 
 
eab29b2
 
 
3ee167a
 
eab29b2
3ee167a
9dd6010
 
 
eab29b2
3ee167a
 
d43ee70
 
3ee167a
 
d43ee70
eab29b2
9dd6010
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cf141c0
9dd6010
 
 
 
 
 
 
 
 
 
eab29b2
9dd6010
543c8b7
ce63d82
eab29b2
cf141c0
9dd6010
 
 
 
 
 
 
667b990
9dd6010
eab29b2
 
9dd6010
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667b990
9dd6010
667b990
9dd6010
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543c8b7
9dd6010
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543c8b7
9dd6010
 
 
 
 
 
 
 
 
543c8b7
9dd6010
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543c8b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3ee167a
543c8b7
 
 
9dd6010
 
 
d43ee70
9dd6010
 
 
d43ee70
 
 
9dd6010
d43ee70
 
 
 
9dd6010
 
 
d43ee70
9dd6010
 
543c8b7
9dd6010
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667b990
9dd6010
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543c8b7
667b990
3ee167a
543c8b7
667b990
d43ee70
3ee167a
d43ee70
eab29b2
543c8b7
3ee167a
afb4867
 
 
 
 
 
 
 
 
 
 
 
 
3ee167a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9dd6010
 
 
 
eab29b2
9dd6010
 
 
eab29b2
 
3ee167a
 
 
 
543c8b7
d43ee70
543c8b7
eab29b2
 
3ee167a
9dd6010
5c9cd93
eab29b2
d43ee70
3ee167a
d43ee70
 
 
 
 
543c8b7
3ee167a
 
 
 
 
 
 
eab29b2
 
eb70d9d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
import os
import subprocess
import gradio as gr
import wget
from ftlangdetect import detect
from cleantext import clean
from keybert import KeyBERT
from keyphrase_vectorizers import KeyphraseCountVectorizer
from sklearn.feature_extraction.text import CountVectorizer
from functools import partial
from sentence_transformers import SentenceTransformer


## models sentence-bert multilingual

# fonte SBERT: https://www.sbert.net/docs/pretrained_models.html#multi-lingual-models
# models na Hugging Face model hub (https://huggingface.co/sentence-transformers/...)
# old: paraphrase-multilingual-MiniLM-L12-v2
model_id = ["paraphrase-multilingual-mpnet-base-v2", "sentence-transformers/LaBSE", "distiluse-base-multilingual-cased-v1"]
model_name = ["SBERT multilingual", "LaBSE", "DistilBERT mltilingual (v1)"]


## get KeyBERT model

kw_model_0 = KeyBERT(model=model_id[0])
#kw_model_1 = KeyBERT(model=model_id[1])
#kw_model_2 = KeyBERT(model=model_id[2])
kw_model = {
    0: kw_model_0,
    #1: kw_model_1,
    #2: kw_model_2
    }


## max_seq_length

# get max_seq_length of the KeyBERT model
#if isinstance(kw_model_0.model.embedding_model, SentenceTransformer):
#  max_seq_length_0 = kw_model_0.model.embedding_model.max_seq_length
# change max_seq_length
#kw_model_0.model.embedding_model.max_seq_length = 512
#num_tokens = kw_model_0.model.embedding_model.tokenize([doc_original])['input_ids'].shape[1]


## spacy (pipeline)

import spacy
# Portuguese pipeline optimized for CPU. Components: tok2vec, morphologizer, parser, lemmatizer (trainable_lemmatizer), senter, ner, attribute_ruler.
spacy_pipeline = "pt_core_news_lg"
# download spacy pipeline (https://spacy.io/models/pt)
os.system(f"python -m spacy download {spacy_pipeline}")

# Load tokenizer, tagger, parser, NER and word vectors
#os.system("python -m spacy download pt_core_news_lg")
nlp = spacy.load(spacy_pipeline)

# Add the component to the pipeline
# "nlp" Object is used to create documents with linguistic annotations.
nlp.add_pipe('sentencizer')


## download stop words in Portuguese
output = subprocess.run(["python", "stopwords.py"], capture_output=True, text=True)
stop_words = list(eval(output.stdout))


## Part-of-Speech Tagging for Portuguese 
# (https://melaniewalsh.github.io/Intro-Cultural-Analytics/05-Text-Analysis/Multilingual/Portuguese/03-POS-Keywords-Portuguese.html)
#pos_pattern = '<NUM.*>*<NOUN.*>*<ADJ.*>*<ADP.*>*<NOUN.*>*<NUM.*>*<NOUN.*>*<ADJ.*>*'
pos_pattern = '<PROPN.*>*<N.*>*<ADJ.*>*'


# vectorizer options
vectorizer_options = ["keyword", "3gramword", "nounfrase"]


# function principal (keywords)
def get_kw_html(doc, top_n, diversity, vectorizer_option, model_id, pos_pattern):

  # lowercase
  lowercase = False

  ## define o vectorizer
  def get_vectorizer(vectorizer_option):
      
    # one word 
    if vectorizer_option == "keyword":    
      vectorizer = CountVectorizer(
          ngram_range=(1, 1),
          stop_words=stop_words, 
          lowercase=lowercase
          )
      
    # upt to 3-gram 
    elif vectorizer_option == "3gramword":    
      vectorizer = CountVectorizer(
          ngram_range=(1, 3),
          #stop_words=stop_words, 
          lowercase=lowercase
          )
      
    # proper noun / noun (adjective) phrase
    elif vectorizer_option == "nounfrase":
      vectorizer = KeyphraseCountVectorizer(
          spacy_pipeline=spacy_pipeline, 
          #stop_words=stop_words, 
          pos_pattern=pos_pattern, 
          lowercase=lowercase
          )   
      
    return vectorizer

  # function to clean text of document
  def get_lang(doc):
    doc = clean(doc,
                fix_unicode=True,               # fix various unicode errors
                to_ascii=False,                  # transliterate to closest ASCII representation
                lower=True,                    # lowercase text
                no_line_breaks=True,           # fully strip line breaks as opposed to only normalizing them
                no_urls=True,                  # replace all URLs with a special token
                no_emails=False,                # replace all email addresses with a special token
                no_phone_numbers=False,         # replace all phone numbers with a special token
                no_numbers=False,               # replace all numbers with a special token
                no_digits=False,                # replace all digits with a special token
                no_currency_symbols=False,      # replace all currency symbols with a special token
                no_punct=False,                 # remove punctuations
                replace_with_punct="",          # instead of removing punctuations you may replace them
                replace_with_url="<URL>",
                replace_with_email="<EMAIL>",
                replace_with_phone_number="<PHONE>",
                replace_with_number="<NUMBER>",
                replace_with_digit="0",
                replace_with_currency_symbol="<CUR>",
                lang="pt"                       # set to 'de' for German special handling
                )
    res = detect(text=str(doc), low_memory=False)
    lang = res["lang"]
    score = res["score"]
  
    return lang, score

  def get_passages(doc):
    # method: https://github.com/UKPLab/sentence-transformers/blob/b86eec31cf0a102ad786ba1ff31bfeb4998d3ca5/examples/applications/retrieve_rerank/in_document_search_crossencoder.py#L19
    doc = doc.replace("\r\n", "\n").replace("\n", " ")
    doc = nlp(doc)

    paragraphs = []
    for sent in doc.sents:
      if len(sent.text.strip()) > 0:
        paragraphs.append(sent.text.strip())

    window_size = 2
    passages = []
    paragraphs = [paragraphs]
    for paragraph in paragraphs:
        for start_idx in range(0, len(paragraph), window_size):
            end_idx = min(start_idx+window_size, len(paragraph))
            passages.append(" ".join(paragraph[start_idx:end_idx]))

    return passages

  # keywords
  def get_kw(doc, kw_model=kw_model[model_id], top_n=top_n, diversity=diversity, vectorizer=get_vectorizer(vectorizer_option)):
    
    keywords = kw_model.extract_keywords(
        doc, 
        vectorizer = vectorizer,
        use_mmr = True,
        diversity = diversity,
        top_n = top_n,
        )
    
    return keywords

  def get_embeddings(doc, candidates, kw_model=kw_model[model_id]):

    doc_embeddings = kw_model.model.embed([doc])
    word_embeddings = kw_model.model.embed(candidates)
  
    # doc_embeddings, word_embeddings = kw_model.extract_embeddings(docs=doc, candidates = candidates,
    #                                                               keyphrase_ngram_range = (1, 100),
    #                                                               stop_words = None,
    #                                                               min_df = 1,
    #                                                               )

    return doc_embeddings, word_embeddings

  # highlight
  def get_html(keywords, doc=doc):

    # ordering of lists (from longest keywords to shortest ones)
    list3 = [keyword[0] for keyword in keywords]
    list2 = [len(item.split()) for item in list3]
    list1 = list(range(len(list2)))
    list2, list1 = (list(t) for t in zip(*sorted(zip(list2, list1))))
    list1 = list1[::-1]
    keywords_list = [list3[idx] for idx in list1]

    # converting doc to html format
    html_doc = doc
    for idx,keyword in enumerate(keywords_list):
      if sum([True if keyword in item else False for item in keywords_list[:idx]]) == 0:
        if keyword not in '<span style="color: black; background-color: yellow; padding:2px">' and keyword not in '</span>':
          html_doc = html_doc.replace(keyword, '<span style="color: black; background-color: yellow; padding:2px">' + keyword + '</span>')
    html_doc = '<p style="font-size:120%; line-height:120%">' + html_doc + '</p>'

    return html_doc

  # if isinstance(kw_model_0.model.embedding_model, SentenceTransformer):
  #   num_tokens = kw_model_0.model.embedding_model.tokenize([doc])['input_ids'].shape[1]


  ## main

  # empty doc
  if len(doc) == 0:

    # get keywords and highlighted text
    keywords, keywords_list_json = [("",0.)], {"":0.}
    html_doc = '<p style="font-size:150%; line-height:120%"></p>'
    label = "O texto do documento não pode estar vazio. Recomece, por favor."

  else:
    
    # detect lang
    lang, score = get_lang(doc)

    # error in lang detect
    if lang!="pt" or score<0.9:

      # get keywords and highlighted text
      keywords, keywords_list_json = [("",0.)], {"":0.}
      html_doc = '<p style="font-size:150%; line-height:120%"></p>'
      label = "O APP não tem certeza de que o texto do documento está em português. Recomece com um texto em português, por favor."
    
    # text not empty and in the correct language
    else:

      # get passages
      passages= get_passages(doc)
      num_passages = len(passages)

      # parameters
      candidates_list = list()
      passages_embeddings = dict()
      candidates_embeddings_list = list()

      # get keywords, candidates and their embeddings
      for i,passage in enumerate(passages):
        keywords = get_kw(passage)

        candidates = [keyword for keyword,prob in keywords]
        candidates_list.extend(candidates)

        passages_embeddings[i], candidates_embeddings = get_embeddings(passage, candidates)
        candidates_embeddings_list.extend(candidates_embeddings)

      if len(candidates_list) > 0:

        # get unique candidates 
        candidates_unique_list = list(set(candidates_list))
        candidates_embeddings_unique_list = [candidates_embeddings_list[candidates_list.index(candidate)] for candidate in candidates_unique_list]
        num_candidates_unique = len(candidates_unique_list)
        
        # get distances between the candidates and respectively all the passages
        # Maximal Marginal Relevance (MMR)
        from keybert._mmr import mmr
        from keybert._maxsum import max_sum_distance
        keywords_list = list()
        for i in range(num_passages):
          keywords_list.append(mmr(passages_embeddings[i],
                                  candidates_embeddings_unique_list,
                                  candidates_unique_list,
                                  num_candidates_unique,
                                  diversity = 0)
          )

        # get the average distances between the candidates and the passages (1 distance by candidate)
        keywords_with_distance_list = dict()
        for i in range(num_passages):
          for keyword, prob in keywords_list[i]:
            if i == 0: keywords_with_distance_list[keyword] = prob
            else: keywords_with_distance_list[keyword] += prob

        # get top_n keywords with prob
        keywords_list_sorted = {k: v for k, v in sorted(keywords_with_distance_list.items(), key=lambda item: item[1], reverse=True)}
        keywords_with_distance_list_sorted = [(keyword, round(keywords_with_distance_list[keyword]/num_passages, 4)) for keyword in keywords_list_sorted]
        keywords_with_distance_list_sorted = keywords_with_distance_list_sorted[:top_n]
      
        # main keyword
        label = f"A palavra/frase chave com a maior similaridade é <span style='color: black; background-color: yellow; padding:2px'>{keywords_with_distance_list_sorted[0][0]}</span>."

        # json for printing
        keywords_list_json = {keyword:prob for keyword, prob in keywords_with_distance_list_sorted}

        # get html doc
        html_doc = get_html(keywords_with_distance_list_sorted)

      else:

        label, keywords_list_json, html_doc = "O APP não encontrou de palavras/frases chave no texto.", {"":0.}, ""

  return label, keywords_list_json, html_doc

def get_kw_html_0(doc, top_n, diversity, vectorizer_option, model_id=0, pos_pattern=pos_pattern):
  return get_kw_html(doc, top_n, diversity, vectorizer_option, model_id, pos_pattern)


title = "Extração das palavras/frases chave em português"

description = '<p>(17/12/2022) Forneça seu próprio texto em português e o APP vai fazer a extração das palavras/frases chave com as maiores similaridades ao texto.</p>\
<p>Este aplicativo usa os modelos seguintes:\
<br />- <a href="https://huggingface.co/sentence-transformers/paraphrase-multilingual-mpnet-base-v2">SBERT multilingual</a>,\
<br />- <a href="https://maartengr.github.io/KeyBERT/index.html">KeyBERT</a> para calcular as similaridades entre as palavras/frases chave e o texto do documento.</p>'

# examples
doc_original_0 = """
As contas de pelo menos seis jornalistas norte-americanos que cobrem tecnologia foram suspensas pelo Twitter na noite desta quinta-feira (15). Os profissionais escrevem sobre o tema para diversos veículos de comunicação dos Estados Unidos, como os jornais 'The New York Times' e 'Washington Post'.

A rede social afirmou apenas que suspende contas que violam as regras, mas não deu mais detalhes sobre os bloqueios.

Assim que comprou o Twitter, Elon Musk disse defender a liberdade de expressão, e reativou, inclusive, a conta do ex-presidente Donald Trump, suspensa desde o ataque ao Capitólio, em 2021.

Os jornalistas que tiveram as contas bloqueadas questionaram o compromisso de Musk com a liberdade de expressão.

Eles encararam o bloqueio como uma retaliação de Musk às críticas que o bilionário vem recebendo pela forma como está conduzindo a rede social: com demissões em massa e o desmonte de áreas, como o conselho de confiança e segurança da empresa.

Metade dos funcionários do Twitter foram demitidos desde que ele assumiu o comando da empresa e outros mil pediram demissão.
      """

doc_original_1 = """
O bilionário Elon Musk restabeleceu neste sábado (17) as contas suspensas de jornalistas no Twitter. A súbita suspensão, um dia antes, provocou reações de entidades da sociedade e setores políticos, além de ameaças de sanção por parte da União Europeia.

O empresário, que comprou a rede social em outubro, acusou os repórteres de compartilhar informações privadas sobre seu paradeiro, sem apresentar provas.

Ainda na sexta (16), o empresário publicou uma enquete na rede social perguntando se as contas deveriam ser reativadas "agora" ou "em sete dias": 58,7% votaram pela retomada imediata; 41,3%, em sete dias.

"O povo falou. Contas envolvidas em doxing (revelação intencional e pública de informações pessoais sem autorização) com minha localização terão sua suspensão suspensa agora", tuitou o empresário neste sábado.

O g1 verificou a conta de alguns dos jornalistas suspensos, que pertencem a funcionários de veículos como a CNN, o The New York Times, o The Washington Post, e as páginas estavam ativas.

As exceções, até a última atualização desta reportagem, eram a conta @ElonJet, que rastreava o paradeiro do jato do próprio Elon Musk, e o perfil do criador, Jack Sweeney.

Ao acessar ambas as páginas, é possível visualizar a seguinte mensagem: "O Twitter suspende as contas que violam as Regras do Twitter".

A plataforma também suspendeu a conta da rede social Mastodon, concorrente do Twitter.

O Twitter Spaces também foi tirado do ar na sexta, após Musk ter sido questionado ao vivo sobre essas últimas decisões, informou a agência de notícias Bloomberg – o bilionário disse que o recurso voltou ao ar na tarde do mesmo dia.
      """

# parameters
num_results = 5
diversity = 0.3

examples = [
    [doc_original_0.strip(), num_results, diversity, vectorizer_options[0]],
    [doc_original_1.strip(), num_results, diversity, vectorizer_options[0]],
    #[doc_original_2.strip(), num_results, diversity, vectorizer_options[0]],
]

# parameters
num_results = 5
diversity = 0.3

# interfaces
interface_0 = gr.Interface(
    fn=get_kw_html_0,
    inputs=[
        gr.Textbox(lines=15, label="Texto do documento"), 
        gr.Slider(1, 20, value=num_results, step=1., label=f"Número das palavras/frases chave a procurar (0: mínimo - 20: máximo - padrão: {num_results})"),
        gr.Slider(0, 1, value=diversity, step=0.1, label=f"Diversidade entre as palavras/frases chave encontradas (0: mínimo - 1: máximo - padrão: {diversity})"),
        gr.Radio(choices=vectorizer_options, value=vectorizer_options[0], label=f"Tipo de resultados (keyword: lista de palavras únicas - 3gramword: lista de 1 a 3 palavras - nounfrase: lista de frases nominais)"),
        ],
    outputs=[
        gr.HTML(label=f"{model_name[0]}"),
        gr.Label(show_label=False),
        gr.HTML(),
        ]
)

# app
demo = gr.Parallel(
    interface_0,
    title=title,
    description=description,
    examples=examples,
    allow_flagging="never"
    )

if __name__ == "__main__":
    demo.launch()