aitech-moj/wyk/02_Jezyki.org

10 KiB

Języki i ich prawa

Jakim rozkładom statystycznym podlegają języki?

Język naturalny albo „Pan Tedeusz” w liczbach

Przygotujmy najpierw „infrastrukturę” do segmentacji tekstu na różnego rodzaju jednostki. Używać będziemy generatorów.

Pytanie Dlaczego generatory zamiast list?

  import requests

  url = 'https://wolnelektury.pl/media/book/txt/pan-tadeusz.txt'
  pan_tadeusz = requests.get(url).content.decode('utf-8')

  pan_tadeusz[100:150]

Księga pierwsza

Gospodarstwo

Powrót pani

Znaki

  from itertools import islice

  def get_characters(t):
      yield from t

  list(islice(get_characters(pan_tadeusz), 100, 150))

['K', 's', 'i', 'ę', 'g', 'a', ' ', 'p', 'i', 'e', 'r', 'w', 's', 'z', 'a', '\r', '\n', '\r', '\n', '\r', '\n', '\r', '\n', 'G', 'o', 's', 'p', 'o', 'd', 'a', 'r', 's', 't', 'w', 'o', '\r', '\n', '\r', '\n', 'P', 'o', 'w', 'r', 'ó', 't', ' ', 'p', 'a', 'n', 'i']

  from collections import Counter

  c = Counter(get_characters(pan_tadeusz))

  c

Counter({' ': 63444, 'a': 30979, 'i': 29353, 'e': 25343, 'o': 23050, 'z': 22741, 'n': 15505, 'r': 15328, 's': 15255, 'w': 14625, 'c': 14153, 'y': 13732, 'k': 12362, 'd': 11465, '\r': 10851, '\n': 10851, 't': 10757, 'm': 10269, 'ł': 10059, ',': 9130, 'p': 8031, 'u': 7699, 'l': 6677, 'j': 6586, 'b': 5753, 'ę': 5534, 'ą': 4794, 'g': 4775, 'h': 3915, 'ż': 3334, 'ó': 3097, 'ś': 2524, '.': 2380, 'ć': 1956, ';': 1445, 'P': 1265, 'W': 1258, ':': 1152, '!': 1083, 'S': 1045, 'T': 971, 'I': 795, 'N': 793, 'Z': 785, 'J': 729, '—': 720, 'A': 698, 'K': 683, 'ń': 651, 'M': 585, 'B': 567, 'O': 567, 'C': 556, 'D': 552, '«': 540, '»': 538, 'R': 489, '?': 441, 'ź': 414, 'f': 386, 'G': 358, 'L': 316, 'H': 309, 'Ż': 219, 'U': 184, '…': 157, '*': 150, '(': 76, ')': 76, 'Ś': 71, 'F': 47, 'é': 43, '-': 33, 'Ł': 24, 'E': 23, '/': 19, 'Ó': 13, '8': 10, '9': 8, '2': 6, 'v': 5, 'Ź': 4, '1': 4, '3': 3, 'x': 3, 'V': 3, '7': 2, '4': 2, '5': 2, 'q': 2, 'æ': 2, 'à': 1, 'Ć': 1, '6': 1, '0': 1})

Napiszmy pomocniczą funkcję, która zwraca listę frekwencyjną.

Counter({' ': 63444, 'a': 30979, 'i': 29353, 'e': 25343, 'o': 23050, 'z': 22741, 'n': 15505, 'r': 15328, 's': 15255, 'w': 14625, 'c': 14153, 'y': 13732, 'k': 12362, 'd': 11465, '\r': 10851, '\n': 10851, 't': 10757, 'm': 10269, 'ł': 10059, ',': 9130, 'p': 8031, 'u': 7699, 'l': 6677, 'j': 6586, 'b': 5753, 'ę': 5534, 'ą': 4794, 'g': 4775, 'h': 3915, 'ż': 3334, 'ó': 3097, 'ś': 2524, '.': 2380, 'ć': 1956, ';': 1445, 'P': 1265, 'W': 1258, ':': 1152, '!': 1083, 'S': 1045, 'T': 971, 'I': 795, 'N': 793, 'Z': 785, 'J': 729, '—': 720, 'A': 698, 'K': 683, 'ń': 651, 'M': 585, 'B': 567, 'O': 567, 'C': 556, 'D': 552, '«': 540, '»': 538, 'R': 489, '?': 441, 'ź': 414, 'f': 386, 'G': 358, 'L': 316, 'H': 309, 'Ż': 219, 'U': 184, '…': 157, '*': 150, '(': 76, ')': 76, 'Ś': 71, 'F': 47, 'é': 43, '-': 33, 'Ł': 24, 'E': 23, '/': 19, 'Ó': 13, '8': 10, '9': 8, '2': 6, 'v': 5, 'Ź': 4, '1': 4, '3': 3, 'x': 3, 'V': 3, '7': 2, '4': 2, '5': 2, 'q': 2, 'æ': 2, 'à': 1, 'Ć': 1, '6': 1, '0': 1})

  from collections import Counter
  from collections import OrderedDict

  def freq_list(g, top=None):
    c = Counter(g)

    if top is None:
       items = c.items()
    else:
       items = c.most_common(top)

    return OrderedDict(sorted(items, key=lambda t: -t[1]))

  freq_list(get_characters(pan_tadeusz), top=8)

OrderedDict([(' ', 63444), ('a', 30979), ('i', 29353), ('e', 25343), ('o', 23050), ('z', 22741), ('n', 15505), ('r', 15328)])

  import matplotlib.pyplot as plt
  from collections import OrderedDict

  def rang_freq_with_labels(name, g, top=None):
     freq = freq_list(g, top)

     plt.figure(figsize=(12, 3))
     plt.bar(freq.keys(), freq.values())

     fname = f'{name}.png'

     plt.savefig(fname)

     return fname

  rang_freq_with_labels('pt-chars', get_characters(pan_tadeusz))

/filipg/aitech-moj/media/commit/cb86308013f2b876b71e952e2efcfe53ddb7cdfc/wyk/pt-chars.png

Słowa

Co rozumiemy pod pojęciem słowa czy wyrazu, nie jest oczywiste. W praktyce zależy to od wyboru tokenizatora.

Załóżmy, że przez wyraz rozumieć będziemy nieprzerwany ciąg liter bądź cyfr (oraz gwiazdek — to za chwilę ułatwi nam analizę pewnego tekstu…).

  from itertools import islice
  import regex as re

  def get_words(t):
    for m in re.finditer(r'[\p{L}0-9\*]+', t):
       yield m.group(0)

  list(islice(get_words(pan_tadeusz), 100, 130))

['Ty', 'co', 'gród', 'zamkowy', 'Nowogródzki', 'ochraniasz', 'z', 'jego', 'wiernym', 'ludem', 'Jak', 'mnie', 'dziecko', 'do', 'zdrowia', 'powróciłaś', 'cudem', 'Gdy', 'od', 'płaczącej', 'matki', 'pod', 'Twoją', 'opiekę', 'Ofiarowany', 'martwą', 'podniosłem', 'powiekę', 'I', 'zaraz']

Zobaczmy 20 najczęstszych wyrazów.

  rang_freq_with_labels('pt-words-20', get_words(pan_tadeusz), top=20)

/filipg/aitech-moj/media/commit/cb86308013f2b876b71e952e2efcfe53ddb7cdfc/wyk/pt-words-20.png

Zobaczmy pełny obraz, już bez etykiet.

  import matplotlib.pyplot as plt
  from math import log

  def rang_freq(name, g):
     freq = freq_list(g)

     plt.figure().clear()
     plt.plot(range(1, len(freq.values())+1), freq.values())

     fname = f'{name}.png'

     plt.savefig(fname)

     return fname

  rang_freq('pt-words', get_words(pan_tadeusz))

/filipg/aitech-moj/media/commit/cb86308013f2b876b71e952e2efcfe53ddb7cdfc/wyk/pt-words.png

Widać, jak różne skale obejmuje ten wykres. Zastosujemy logartm, najpierw tylko do współrzędnej y.

  import matplotlib.pyplot as plt
  from math import log

  def rang_log_freq(name, g):
     freq = freq_list(g)

     plt.figure().clear()
     plt.plot(range(1, len(freq.values())+1), [log(y) for y in freq.values()])

     fname = f'{name}.png'

     plt.savefig(fname)

     return fname

  rang_log_freq('pt-words-log', get_words(pan_tadeusz))

/filipg/aitech-moj/media/commit/cb86308013f2b876b71e952e2efcfe53ddb7cdfc/wyk/pt-words-log.png

Pytanie Dlaczego widzimy coraz dłuższe „schodki”?

Hapax legomena

Z poprzedniego wykresu możemy odczytać, że ok. 2/3 wyrazów wystąpiło dokładnie 1 raz. Słowa występujące jeden raz w danym korpusie noszą nazwę hapax legomena (w liczbie pojedycznej hapax legomenon, ἅπαξ λεγόμενον, „raz powiedziane”, żargonowo: „hapaks”).

„Prawdziwe” hapax legomena, słowa, które wystąpiły tylko raz w całym korpusie tekstów danego języka (np. starożytnego) rzecz jasna sprawiają olbrzymie trudności w tłumaczeniu. Przykładem jest greckie słowo ἐπιούσιος, przydawka odnosząca się do chleba w modlitwie „Ojcze nasz”. Jest to jedyne poświadczenie tego słowa w całym znanym korpusie greki (nie tylko z Pisma Świętego). W języku polskim tłumaczymy je na „powszedni”, ale na przykład w rosyjskim przyjął się odpowiednik „насущный” — o przeciwstawnym do polskiego znaczeniu!

W sumie podobne problemy hapaksy mogą sprawiać metodom statystycznym przy przetwarzaniu jakiekolwiek korpusu.

Wykres log-log

Jeśli wspomniany wcześniej wykres narysujemy używając skali logarytmicznej dla obu osi, otrzymamy kształt zbliżony do linii prostej.

Tę własność tekstów nazywamy prawem Zipfa.

  import matplotlib.pyplot as plt
  from math import log

  def log_rang_log_freq(name, g):
     freq = freq_list(g)

     plt.figure().clear()
     plt.plot([log(x) for x in range(1, len(freq.values())+1)], [log(y) for y in freq.values()])

     fname = f'{name}.png'

     plt.savefig(fname)

     return fname

  log_rang_log_freq('pt-words-log-log', get_words(pan_tadeusz))

/filipg/aitech-moj/media/commit/cb86308013f2b876b71e952e2efcfe53ddb7cdfc/wyk/pt-words-log-log.png

Tajemniczy język Manuskryptu Wojnicza

Manuskrypt Wojnicza to powstały w XV w. manuskrypt spisany w tajemniczym alfabecie, do dzisiaj nieodszyfrowanym. Rękopis stanowi jedną z największych zagadek historii (i lingwistyki).

Źródło: https://commons.wikimedia.org/wiki/File:Voynich_Manuscript_(135).jpg

Sami zbadajmy statystyczne własności tekstu manuskryptu. Użyjmy transkrypcji Vnow, gdzie poszczególne znaki tajemniczego alfabetu zamienione na litery alfabetu łacińskiego, cyfry i gwiazdkę. Jak transkrybować Manuskrypt, pozostaje sprawą dyskusyjną, natomiast wybór takiego czy innego systemu transkrypcji nie powinien wpływać dramatycznie na analizę statystyczną.

  import requests

  voynich_url = 'http://www.voynich.net/reeds/gillogly/voynich.now'
  voynich = requests.get(voynich_url).content.decode('utf-8')

  voynich = re.sub(r'\{[^\}]+\}|^<[^>]+>|[-# ]+', '', voynich, flags=re.MULTILINE)

  voynich = voynich.replace('\n\n', '#')
  voynich = voynich.replace('\n', ' ')
  voynich = voynich.replace('#', '\n')

  voynich = voynich.replace('.', ' ')

  voynich[100:150]

9 OR 9FAM ZO8 QOAR9 Q*R 8ARAM 29 [O82*]OM OPCC9 OP

  rang_freq_with_labels('voy-chars', get_characters(voynich))

/filipg/aitech-moj/media/commit/cb86308013f2b876b71e952e2efcfe53ddb7cdfc/wyk/voy-chars.png

  log_rang_log_freq('voy-log-log', get_words(voynich))

/filipg/aitech-moj/media/commit/cb86308013f2b876b71e952e2efcfe53ddb7cdfc/wyk/voy-log-log.png

  rang_freq_with_labels('voy-words-20', get_words(voynich), top=20)

/filipg/aitech-moj/media/commit/cb86308013f2b876b71e952e2efcfe53ddb7cdfc/wyk/voy-words-20.png

    log_rang_log_freq('voy-words-log-log', get_words(voynich))

/filipg/aitech-moj/media/commit/cb86308013f2b876b71e952e2efcfe53ddb7cdfc/wyk/voy-words-log-log.png