Ten dokument opisuje przyjęty sposób reprezentacji danych związanych z anotacjami (fraz, bytów nazwanych, wyrażeń wielosłowowych) oraz spraw z tym związanych.

Potrzebne struktury danych
  • zdanie, token, leksem, tag, tagset — potrzebne do reprezentacji tekstu podzielonego na tokeny i jego oznakowanie morfo-syntaktycznego; założenie: używamy struktur danych z Corpus2
  • kanał — zawiera niezagnieżdżone anotacje (chunki, byty nazwane, jednostki wielowyrazowe itp.) zgrupowane pod jedną nazwą (np. NP czy Person),
  • opis anotacji — określa dodatkowe cechy anotacji: lokalizację nadrzędnika, opcjonalnie tag i lemat, atrybuty (klucz–wartość)
  • relacja — nazwany zbiór powiązań między anotacjami (mogą być z różnych kanałów)
  • rzutowane zdanie — udaje oznakowane zdanie (ma ten sam interfejs), reprezentuje zdanie z punktu widzenia któregoś z kanałów; np. rzutowanie po frazach NP powoduje, że oznaczone frazy zachowują się jak tokeny, a tokeny nie należące do NP pozostają na swoich miejscach

Przypadki użycia uzasadniające obecność tych struktur

  1. Automatyczne znakowanie anotacji (chunków — fraz oznakowanych jako anotacje w kanale)
    • pobieranie znacznika IOB2 na podanej pozycji w kanale o podanej nazwie
    • ustawianie znacznika IOB2 na podanej pozycji w kanale o podanej nazwie
    • zapewnienie spójności danych w kanale (albo samoczynnie, albo naprawienie post factum)
  2. Znakowanie anotacji za pomocą reguł pisanych ręcznie
    • Reguła dopasowuje tokeny i oznacza taki zakres jako anotację
    • Reguła dopasowuje się do oznaczonej już anotacji
  3. Znakowanie relacji za pomocą reguł pisanych ręcznie
    • Regułą dopasowuje się do oznaczonych już anotacji i oznacza instancję relacji między nimi
  4. Znakowanie nieciągłych anotacji regułami ręcznymi
    • Chcemy oznakować wyrażenie wielosłowowe, gdy w tekście jest rodzielone czymś (transporter jedzie opancerzony)
  5. Znakowanie nieciągłych anotacji automatycznie
    • Chcemy uczyć chunker rozpoznawać nieciągłe frazy uzgodnione (znane nam dzieła)
    • Chcemy odpalić chunker rozpoznający takie frazy
  6. Rzutowanie po kanale
    • Chcemy traktować wyrażenia wielosłowowe na równi z tokenami (i odpalać na nich jakiś wyrażenia JOSKIPI albo po prostu czytać tokeny, tagi i lematy)
    • Chcemy znakować relacje między anotacjami, traktując je na równi z tokenami
    • Chcemy traktować konstrukcje zleksykalizowane (na przykład, in plus, ze względu na) na równi z tokenami i uczyć/testować na tym chunker właściwy

Podstawowe założenia modelu:

  1. Opis morfo-syntaktyczny tokenów a anotacje różnią się istotnie, zatem te informacje trzymane są osobno (opis m-s to zbiór leksemów i orth per token).
  2. Anotacje gromadzone są w niezwiązanych ze sobą „kanałach” (powiązania mogą być jedynie na poziomie relacji).
  3. Kanały reprezentowane wewnętrznie są jako ciąg numerów
  4. Kanał potrafi przedstawić się w formacie IOB2 możemy pobrać tag IOB2 dla podanej pozycji
  5. Kanał pozwala zmienić swoją zawartość na podstawie podanej pozycji i decyzji (do ustalenia: albo decyzja w stylu „zacznij tu nowy chunk”, albo „wstaw tu podany tag IOB2 i zapewnij spójność danych”)
  6. Ten sam kanał oglądany z wyższego poziomu abstrakcji składa się z anotacji (chunków). Jedna anotacja odpowiada każdemu ciągowi tagów BI* (I* zachłannie).
  7. Gdy zachodzi potrzeba odwołania się do konkretnej anotacji, podajemy pozycję pierwszego jej tokenu (na poziomie IOB2 będzie tam B).
  8. Każda anotacja ma określony numer (odpowiada on numerom zawartym w wewnętrzej reprezentacji kanału). Zazwyczaj każda anotacja ma unikalny numer
  9. Jeśli utworzymy dwie anotacje o tym samym numerze (niezerowym), uzyskujemy opis frazy nieciągłej. Dlaczego taka reprezentacja: ten zabieg zwykle nie jest potrzebny, więc najczęstsze rzeczy opisujemy w sposób prosty. Na poziomie reprezentacji danych mamy ciąg numerów, gdzie brak anotacji to zero. Dwie anotacje są ze sobą powiązane numerem wtt gdy mają ten sam niezerowy numer i leżą w tym samym kanale (choć to ostatnie założenie można w razie potrzeby anulować).
  10. Anotacja może być opisana bardziej szczegółowo: można określić jej nadrzędnik, tagi i lematy oraz dodatkowe atrybuty (słownik klucz–wartość, gdzie klucze i wartości są stringami). Ten opis jest opcjonalny, realizowany jest w pomocniczej strukturze podpiętej do kanału.

Zdanie

TODO: musimy się zdecydować na jedną z opcji:
  • rozszerzamy obecne zdanie o anotacje i relacje,
  • tworzymy nową klasę z tymi rozszerzeniami w ramach corpus2
  • j.w., ale tworzymy osobną bibliotekę na to wszystko
Struktura zawiera:
  • informacją morfo-syntaktyczną (obowiązkowo; to już mamy): ciąg tokenów, każdy token to Corpus2::Token; token można opisać jako para (forma napotkana — orth, ciąg leksemów — par (lemat, tag, [_czy rozstrzygający_])). Atrybut czy rozstrzygający zostawiamy, bo tak reprezentowane są dane w Corpus2 (XML-u i TaKIPI), ale wartość tej flagi nie będzie brana pod uwagę przez JOSKIPI (nie będzie w ogóle dostępna z poziomu języka).
  • anotacje w nazwanych kanałach (opcjonalnie): nazwa kanału -> kanał (struktura opisana poniżej)
  • relacje między anotacjami (opcjonalnie): nazwa relacji -> relacja (struktura opisana poniżej)

Opcjonalność oznacza, że może być zdanie bez ani jednego kanału i bez żadnych relacji, natomiast zdanie bez tokenów jest wadliwe.

Fizycznie zdanie zawiera:
  • listę tokenów (już mamy),
  • mapę nazwa kanału -> obiekt Channel
  • mapę nazwa relacji -> obiekt Relation (opisuje zbiór instancji relacji)

Kanał

TODO czy kanał musi znać swoją nazwę? (może wystarczy nazwa w mapie)

Kanał zawiera:
  • listę liczb całkowitych odpowiadających numerom anotacji
  • mapę/listę opisów anotacji (może być pusta)

Opis anotacji

To, że istnieje anotacja, wynika przede wszystkim z danych w kanale (fizycznie mamy tam ciąg niezerowych wartości). Informacja ta jest zwięzła i pozwala na wygodne tworzenie nowych anotacji przez chunker. Oprócz tego dla każdego numeru anotacji może istnieć obiekt „opis anotacji”, który opisuje szczegóły:
  • lokalizację nadrzędnika (liczbę określającą położenie nadrzędnika względem pierwszego tokenu)
  • opcjonalnie ciąg leksemów opisujących całą anotację (leksem == para tag i lemat; chodzi o używaną przez token strukturę z corpus2)
  • opcjonalne atrybuty (mapa klucz:string -> wartość:string)

Nadrzędnik (ang. head) z założenia ma być tokenen, który składniowo reprezentuje całą anotację (tj. anotację można zredukować do nadrzędnika i dalej zdanie będzie sensowne). W praktyce można oczywiście używać go do dowolnych celów.

Mechanizm rzutowania po kanale

Mechanizm rzutowania pozwala spojrzeć na anotacje z punktu widzenia danego kanału. Idea: jeśli oznakujemy frazy rzeczownikowe, chcemy mieć potem możliwość traktowania ich jako pojedyncze tokeny. Rzutowane zdanie zachowuje się jak oryginalne zdanie, lecz ciągi tokenów odpowiadające anotacjom zamieniane są przez szutczne tokeny.

Mechanizm rzutowania jako taki nie należy do języka. Powinien być udostępniany przez API, można by go użyć po oznakowaniu tekstu, może być też przydatny po jednym etapie anotacji, ale przed odpaleniem kolejnych operatorów JOSKIPI.

Wejście: oznakowane zdanie, nazwa kanału, po którym rzutujemy.

Pre-conditions:
  • dla każdego numeru anotacji musi istnieć jeden nadrzędnik (tj. anotacje o unikalnych numerach mają po nadrzędniku),
Wyjście: obiekt udający zdanie (różnica: mamy dodatkowo funkcję „commit”, o niej później). Obiekt zawiera:
  • zmienioną listę tokenów z anotacjami; tokeny, które nie należały do anotacji w podanym kanale, pozostają niezmienione; tokeny, które należały do anotacji o podanym numerze (niekoniecznie ciągłej) są scalane w jeden token i wstawiane w miejsce nadrzędnika anotacji
  • orthy scalonych tokenów powstają w wyniku złączenia orthów kolejnych tokenów za pomocą spacji (uwzględniając informację o tym, czy spacja wystąpiła)
  • jeśli anotacja miała przypisane leksemy (tj. tagi i lematy), to te informacje wstawiane są do tokenu
  • jeśli nie miała, brana jest informacja z tokenu będącego nadrzędnikiem
  • obiekt udający zdanie pamięta, które jego tokeny pochodzą od których tokenów ze zdania źródłowego (mamy indeksy), a które wynikły ze złączenia których tokenów (też mamy indeksy, lecz tutaj są to indeksy wielu tokenów na jeden token wynikowy); co więcej, pamięta przekształcenie z indeksów nowego zdania w indeksy zdania starego (a w zasadzie dwa przekształcenia: jedno zwracające pierwszy token, drugie — ostatni; to rozróżnienie ma tylko znaczenie dla pozycji dotkniętych rzutowaniem).
  • podany kanał zostaje, jest również w analogiczny sposób rzutowany, tj. jedynie zawiera jedynie jednotokenowe anotacjach w miejscach, gdzie były nadrzędniki,
  • pozostałe kanały poddawane są rzutowaniu w następujący sposób:
    • jeśli kanał zawiera tylko anotacje w miejscach, które nie uległy zmianie, to anotacje te są przenoszone bezpośrednio,
    • TODO ustalić, co dzieje się w trudniejszych wypadkach (niezbędne minimum, to wywalić te kanały, które nam nie pasują)

Od tej pory omówiony sposób ustalania leksemów przypisanych do anotacji (tj. sprawdzenie czy są one określone wprost, a jeśli nie, to wzięcie ich z nadrzędnika) nazywać będziemy „reprezentacją leksemową anotacji”.

Rzutowane zdanie pozwala na wykonanie operacji commit, która próbuje przenieść zmiany na nim dokonane na zdanie oryginalne (TODO: ustalić czy rzutowane zdanie trzyma zdanie oryginalne, czy trza je przekazać jako parametr operacji). Operacja ta dokonuje następujących zmian:
  • tokeny na pozycjach niedotkniętych rzutowaniem są kopiowane w miejsca oryginalne,
  • procedura dla pozostałych tokenów:
    • dla każdego takiego tokenu znajdujemy odpowiadającą mu anotację,
    • jeśli anotacja ma przypisane leksemy, to zamieniamy je na leksemy naszego tokenu,
    • jeśli nie, leksemy nadrzędnika anotacji zamieniamy na leksemy naszego tokenu;
  • TODO to wersja uproszczona, może da się zrobić coś lepszego dla trudniejszych wypadków, by nie popsuć tego, co było:
    • czyścimy wszystkie anotacje w zdaniu oryginalnym,
    • dla każdego kanału, dla każdej anotacji:
      • „rysujemy” anotację począwszy od Tp(początek) do Tk(koniec), gdzie Tp,Tk to przekształcenia z indeksowania obiektu rzutowanego zdania w indeksowanie oryginalnego zdania (Tp to przekształcenie w pierwszy token, Tk — w ostatni).

Prosty przykład rzutowania: mamy zdanie „Nie jadam [podsuszanych wiejskich kiełbas.” z kanałem NP (mamy jedną frazę: [podsuszanych wiejskich kiełbas], gdzie ostatni token jest nadrzędnikiem). Po rzutowaniu otrzymujemy następujący ciąg tokenów (sklejony token jest pokryty jednotokenową anotacją):

Nie
jadam
[podsuszanych wiejskich kiełbas]
.