KWT-2024/lab/lab_15.ipynb
2024-05-25 14:53:24 +02:00

22 KiB
Raw Blame History

Logo 1

Komputerowe wspomaganie tłumaczenia

15. Korekta gramatyczna [laboratoria]

Rafał Jaworski (2021)

Logo 2

Ostatnią z omawianych przez nas technik stosowaną podczas wspomagania tłumaczenia jest korekta gramatyczna. Automatyczna korekta gramatyczna tekstu to ambitne zadanie odnalezienia możliwych błędów niezwiązanych bezpośrednio z pisownią. Są to między innymi:

  • błędy gramatyczne
  • źle użyte słowa
  • złe połączenia wyrazowe
  • błędy interpunkcyjne
  • kolokwializmy
  • redundancja (np. "tylko i wyłącznie")

Warto zwrócić uwagę, iż systemy do korekcji gramatycznej można traktować jako klasyfikatory binarne. Przyjmijmy, że odpowiedź pozytywna korektora to wykrycie błędu w tekście, natomiast negatywna - brak błędu. Wówczas rozróżniamy dwa typy pomyłek: false positive oraz false negative. False positive to tzw. "fałszywy alarm" - zbyt duża ich ilość spowoduje wydłużenie czasu pracy użytkownika przez konieczność analizowania zgłoszeń, które w istocie błędami nie są. Co jednak jeszcze gorsze, zbyt duża ilość false positives powoduje spadek zaufania użytkownika do systemu oraz drastyczny spadek satysfakcji z używania systemu. Te drugie błędy - false negatives - to z kolei faktyczne błędy w tekście, które nie zostały wyłapane przez system korekty. Stare polskie porzekadło głosi, że "czego oko nie widzi, tego sercu nie żal". Niestety jednak problem pojawia się, kiedy dostrzeże to jakieś inne oko... Wysoka liczba false negatives wprawdzie skraca czas korekty (sic!), ale odbywa się to kosztem jakości całego procesu. Idealnie zatem byłoby zminimalizować zarówno liczbę false positives, jak i false negatives. Jak jednak łatwo się domyślić, nie jest to zawsze możliwe. Korektor gramatyczny, który jest bardzo restrykcyjny i raportuje wiele błędów, będzie miał tendencję do popełniania false positives. Natomiast korektor bardziej pobłażliwy niechybnie popełni wiele false negatives. Co zatem jest ważniejsze? Praktyka wskazuje, że oba parametry mają podobną wagę, ale jednak odrobinę ważniejsze jest powstrzymanie się od false positives.

Do najbardziej popularnych narzędzi wspomagających korektę gramatyczną tekstu należą Grammarly oraz LanguageTool. Na dzisiejszych zajęciach zajmiemy się tym drugim. LanguageTool został pierwotnie napisany jako praca dyplomowa Daniela Nabera, a następnie intensywnie rozwijany wspólnie z Marcinem Miłkowskim. Aż do dziś projekt jest ciągle rozwijany, zwiększana jest liczba obsługiwanych języków oraz dokładność działania.

LanguageTool jest systemem opartym na regułach. W dobie wszechobecnej sztucznej inteligencji opartej na uczeniu maszynowym rozwiązanie to wydaje się nieco przestarzałe. Jednak to właśnie reguły stanowią o sile LanguageToola - pozwalają one na zwiększenie precyzji korektora poprzez minimalizację false positives. Warto wspomnieć, iż liczne reguły LanguageToola dotyczą również korekty pisowni. Czyni to z LanguageToola kompletne narzędzie do korekty tekstu. Polecam przejrzenie zestawu reguł LanguageToola dla języka angielskiego: https://community.languagetool.org/rule/list?lang=en

Czas uruchomić to narzędzie. Skorzystajmy z Pythona.

pip3 install language_tool_python
!pip3 install language_tool_python
Collecting language_tool_python
  Downloading language_tool_python-2.8-py3-none-any.whl (35 kB)
Requirement already satisfied: pip in /usr/local/lib/python3.10/dist-packages (from language_tool_python) (23.1.2)
Requirement already satisfied: requests in /usr/local/lib/python3.10/dist-packages (from language_tool_python) (2.31.0)
Requirement already satisfied: tqdm in /usr/local/lib/python3.10/dist-packages (from language_tool_python) (4.66.4)
Requirement already satisfied: wheel in /usr/local/lib/python3.10/dist-packages (from language_tool_python) (0.43.0)
Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests->language_tool_python) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests->language_tool_python) (3.7)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests->language_tool_python) (2.0.7)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/dist-packages (from requests->language_tool_python) (2024.2.2)
Installing collected packages: language_tool_python
Successfully installed language_tool_python-2.8

Następnie możemy użyć LanguageToola w programie Pythonowym: (przykład zaczerpnięty z oficjalnego tutoriala: https://pypi.org/project/language-tool-python/)

import language_tool_python
import pprint
tool = language_tool_python.LanguageTool('en-US')

text = 'A sentence with a error in the Hitchhikers Guide tot he Galaxy'

pp = pprint.PrettyPrinter(depth=2)
errors = tool.check(text)
pp.pprint(errors)
Downloading LanguageTool 6.4: 100%|██████████| 246M/246M [00:08<00:00, 29.1MB/s]
INFO:language_tool_python.download_lt:Unzipping /tmp/tmpk9z9693r.zip to /root/.cache/language_tool_python.
INFO:language_tool_python.download_lt:Downloaded https://www.languagetool.org/download/LanguageTool-6.4.zip to /root/.cache/language_tool_python.
[Match({'ruleId': 'EN_A_VS_AN', 'message': 'Use “an” instead of a if the following word starts with a vowel sound, e.g. an article, an hour.', 'replacements': ['an'], 'offsetInContext': 16, 'context': 'A sentence with a error in the Hitchhikers Guide tot he ...', 'offset': 16, 'errorLength': 1, 'category': 'MISC', 'ruleIssueType': 'misspelling', 'sentence': "A sentence with a error in the Hitchhiker's Guide tot he Galaxy"}),
 Match({'ruleId': 'TOT_HE', 'message': 'Did you mean “to the”?', 'replacements': ['to the'], 'offsetInContext': 43, 'context': '... with a error in the Hitchhikers Guide tot he Galaxy', 'offset': 50, 'errorLength': 6, 'category': 'TYPOS', 'ruleIssueType': 'misspelling', 'sentence': "A sentence with a error in the Hitchhiker's Guide tot he Galaxy"})]

Przeanalizujmy format zwracanego wyniku. Otrzymujemy listę obiektów Match - zawiadomień o potencjalnym błędzie. Razem z każdym błędem otrzymujemy m.in. identyfikator użytej reguły, opis błędu, rekomendancję poprawy, kontekst.

Ćwiczenie 1: Użyj LanguageTool do znalezienia jak największej liczby prawdziwych błędów na swoim ulubionym portalu internetowym. Skorzystaj z poznanych wcześniej technik web scrapingu. Uwaga - LanguageTool najprawdopodobniej oznaczy nazwy własne jako literówki - ten typ błędu nie powinien być brany pod uwagę.

import requests
from bs4 import BeautifulSoup
import regex
import language_tool_python
import pprint

def sentence_split(text):
    pattern = r'(?=\p{Lu})'
    segments = regex.split(pattern, text)
    segments = [segment.strip() for segment in segments if segment.strip()]
    return segments

url = 'https://amu.edu.pl/dla-mediow/komunikaty-prasowe/czy-dzikie-pszczoly-zagrazaja-pszczolom-miodnym'
page = requests.get(url)
soup = BeautifulSoup(page.content, 'html.parser')

article_content = soup.find('div', {'class': 'news__body'}).get_text()

sentences = sentence_split(article_content)

tool = language_tool_python.LanguageTool('pl')

def is_proper_noun_error(error):
    return 'MORFOLOGIA' in error.ruleId and 'Proper noun' in error.message

all_errors = []
for sentence in sentences:
    errors = tool.check(sentence)
    filtered_errors = [error for error in errors if not is_proper_noun_error(error)]
    all_errors.extend(filtered_errors)

pp = pprint.PrettyPrinter(depth=2)
pp.pprint(all_errors)
[Match({'ruleId': 'DYWIZ', 'message': 'Spacje wokół dywizu (w przeciwieństwie do myślnika) są zbędne: "możliwa-mówi"; jeśli to miał być myślnik, to należy napisać "możliwa — mówi".', 'replacements': ['możliwa-mówi', 'możliwa — mówi'], 'offsetInContext': 43, 'context': '...egatywna interakcja w drugą stronę jest możliwa - mówi dr', 'offset': 105, 'errorLength': 14, 'category': 'TYPOGRAPHY', 'ruleIssueType': 'typographical', 'sentence': 'Tak przewrotnie postawione pytanie zmusza do zastanowienia, czy negatywna interakcja w drugą stronę jest możliwa - mówi dr'}),
 Match({'ruleId': 'MORFOLOGIK_RULE_PL_PL', 'message': 'Wykryto prawdopodobny błąd pisowni', 'replacements': ['Wędzonka', 'Wędzonką', 'Wędzona', 'Wędzonki', 'Wędzonko', 'Wędzonkę', 'Wędzoną', 'Wędząca', 'Wędzącą'], 'offsetInContext': 0, 'context': 'Wendzonka.', 'offset': 0, 'errorLength': 9, 'category': 'TYPOS', 'ruleIssueType': 'misspelling', 'sentence': 'Wendzonka.'}),
 Match({'ruleId': 'PRZECINEK_ANI', 'message': 'Prawdopodobnie zbędny przecinek, powinno być: " i".', 'replacements': [' i'], 'offsetInContext': 8, 'context': 'Pszczoły, i dzikie i miodne, ewoluowały i współwyst...', 'offset': 8, 'errorLength': 3, 'category': 'PUNCTUATION', 'ruleIssueType': 'typographical', 'sentence': 'Pszczoły, i dzikie i miodne, ewoluowały i współwystępowały razem przez miliony lat.'}),
 Match({'ruleId': 'PL_UNPAIRED_BRACKETS', 'message': 'Brak niesparowanego symbolu: „(”', 'replacements': [], 'offsetInContext': 14, 'context': 'Apis mellifera) jako gatunek społeczny, magazynujący za...', 'offset': 14, 'errorLength': 1, 'category': 'PUNCTUATION', 'ruleIssueType': 'typographical', 'sentence': 'Apis mellifera) jako gatunek społeczny, magazynujący zapasy i potrafiący wielokrotnie zimować w gnieździe jako rodzina, jest na szczycie drabiny ewolucyjnej.'}),
 Match({'ruleId': 'POTRAFIACY', 'message': 'Czasownik „potrafić” nie ma imiesłowu czynnego. Poprawnie: "umiejący".', 'replacements': ['umiejący'], 'offsetInContext': 43, 'context': '...atunek społeczny, magazynujący zapasy i potrafiący wielokrotnie zimować w gnieździe jako r...', 'offset': 62, 'errorLength': 10, 'category': 'STYLE', 'ruleIssueType': 'style', 'sentence': 'Apis mellifera) jako gatunek społeczny, magazynujący zapasy i potrafiący wielokrotnie zimować w gnieździe jako rodzina, jest na szczycie drabiny ewolucyjnej.'}),
 Match({'ruleId': 'NIE_TYLE_ALE', 'message': 'Czy chodziło o "ile"? (Zdanie występuje po spójniku skorelowanym „nie tyle...”).', 'replacements': ['ile'], 'offsetInContext': 43, 'context': '...e stawką jest życie nie tyle osobników, ale całych gatunków, na częstotliwości taki...', 'offset': 67, 'errorLength': 3, 'category': 'SYNTAX', 'ruleIssueType': 'grammar', 'sentence': 'Jednakże w przyrodzie, gdzie stawką jest życie nie tyle osobników, ale całych gatunków, na częstotliwości takich wyczynów nie można polegać.'}),
 Match({'ruleId': 'MORFOLOGIK_RULE_PL_PL', 'message': 'Wykryto prawdopodobny błąd pisowni', 'replacements': [], 'offsetInContext': 43, 'context': '...anie kwestii kontroli zagęszczenia uli (napszczelenia) staje się zadaniem najważniejszym, tak...', 'offset': 113, 'errorLength': 13, 'category': 'TYPOS', 'ruleIssueType': 'misspelling', 'sentence': 'Jeżeli mamy poważnie podchodzić do ochrony dzikich zapylaczy, to uregulowanie kwestii kontroli zagęszczenia uli (napszczelenia) staje się zadaniem najważniejszym, także na poziomie legislacyjnym.'}),
 Match({'ruleId': 'DYWIZ', 'message': 'Spacje wokół dywizu (w przeciwieństwie do myślnika) są zbędne: "życzę-dodaje"; jeśli to miał być myślnik, to należy napisać "życzę — dodaje".', 'replacements': ['życzę-dodaje', 'życzę — dodaje'], 'offsetInContext': 43, 'context': '...i, czego wszystkim pszczołom w ich dniu życzę - dodaje naukowiec z', 'offset': 119, 'errorLength': 14, 'category': 'TYPOGRAPHY', 'ruleIssueType': 'typographical', 'sentence': 'Właśnie w tym aspekcie musi się dokonać największa zmiana w ludzkiej mentalności, czego wszystkim pszczołom w ich dniu życzę - dodaje naukowiec z'}),
 Match({'ruleId': 'MORFOLOGIK_RULE_PL_PL', 'message': 'Wykryto prawdopodobny błąd pisowni', 'replacements': ['Wykryta', 'Wykrytą', 'Wąkrota', 'Wykroją', 'Wykrot', 'Wykrotu', 'Wykroty', 'Wąkrotą', 'Wyk rota', 'Wykrot a'], 'offsetInContext': 0, 'context': 'Wykrota', 'offset': 0, 'errorLength': 7, 'category': 'TYPOS', 'ruleIssueType': 'misspelling', 'sentence': 'Wykrota'})]

Ćwiczenie 2: Napisz skrypt, który poszuka błędów w komentarzach klasy Javowej (zwykłych // oraz w javadocach). Uruchom ten skrypt na źródłach wybranego opensourcowego projektu w Javie.

!pip install gitpython
Collecting gitpython
  Downloading GitPython-3.1.43-py3-none-any.whl (207 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 207.3/207.3 kB 1.5 MB/s eta 0:00:00
[?25hCollecting gitdb<5,>=4.0.1 (from gitpython)
  Downloading gitdb-4.0.11-py3-none-any.whl (62 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.7/62.7 kB 7.6 MB/s eta 0:00:00
[?25hCollecting smmap<6,>=3.0.1 (from gitdb<5,>=4.0.1->gitpython)
  Downloading smmap-5.0.1-py3-none-any.whl (24 kB)
Installing collected packages: smmap, gitdb, gitpython
Successfully installed gitdb-4.0.11 gitpython-3.1.43 smmap-5.0.1
import requests
import re
import language_tool_python
from bs4 import BeautifulSoup
from pathlib import Path

def extract_comments_from_java(content):
    comments = []
    single_line_comment_pattern = re.compile(r'//.*')
    multi_line_comment_pattern = re.compile(r'/\*[\s\S]*?\*/')

    single_line_comments = single_line_comment_pattern.findall(content)
    multi_line_comments = multi_line_comment_pattern.findall(content)
    comments.extend(single_line_comments)
    comments.extend(multi_line_comments)

    return comments

def check_comments_for_errors(comments, language_tool):
    all_errors = []
    for comment in comments:
        errors = language_tool.check(comment)
        all_errors.extend(errors)
    return all_errors

def correct_java_grammar(java_file_path):
    response = requests.get(java_file_path)
    if response.status_code != 200:
        print(f"Problem z otwarciem strony: {java_file_path}")
        return []

    content = response.text
    comments = extract_comments_from_java(content)
    tool = language_tool_python.LanguageTool('en-US')
    errors = check_comments_for_errors(comments, tool)

    return errors

def fetch_java_files(url):
    response = requests.get(url)
    if response.status_code != 200:
        print(f"Problem z otwarciem strony: {url}")
        return []

    soup = BeautifulSoup(response.content, 'html.parser')
    java_files = []

    for link in soup.find_all('a', href=True):
        href = link['href']
        if href.endswith('.java'):
            java_files.append(f"https://raw.githubusercontent.com{href.replace('/blob/', '/')}")

    return java_files

github_url = 'https://github.com/gavalian/groot/tree/master/src/main/java/org/jlab/jnp/groot'
java_files = fetch_java_files(github_url)

for java_file in java_files:
    errors = correct_java_grammar(java_file)
    if errors:
        print(f"Errors in {java_file}:")
        for error in errors:
            print(f'Error: {error.message}')
            print(f'Sentence: {error.context}')
            print(f'Rule: {error.ruleId}')
            print()