Wykrywanie sylab w słowie

138

Muszę znaleźć dość skuteczny sposób wykrywania sylab w słowie. Na przykład,

Niewidoczne -> in-vi-sib-le

Istnieje kilka zasad sylabowania, których można użyć:

V CV VC CVC CCV CCCV CVCC

* gdzie V to samogłoska, a C to spółgłoska. Na przykład,

Wymowa (5 Pro-nun-ci-a -ation; CV-CVC-CV-V-CVC)

Wypróbowałem kilka metod, wśród których było użycie wyrażenia regularnego (które pomaga tylko wtedy, gdy chcesz liczyć sylaby) lub zdefiniowanie reguły na sztywno (podejście brutalnej siły, które okazuje się bardzo nieefektywne) i wreszcie użycie automatów skończonych (które nie skutkuje niczym przydatnym).

Celem mojej aplikacji jest stworzenie słownika zawierającego wszystkie sylaby w danym języku. Słownik ten będzie później używany do sprawdzania pisowni (przy użyciu klasyfikatorów Bayesa) oraz do syntezy tekstu na mowę.

Byłbym wdzięczny, gdyby ktoś mógł dać mi wskazówki dotyczące alternatywnego sposobu rozwiązania tego problemu, oprócz moich poprzednich podejść.

Pracuję w Javie, ale każda wskazówka w C / C ++, C #, Pythonie, Perlu ... zadziała.

user50705
źródło
Czy chcesz poznać rzeczywiste punkty podziału, czy tylko liczbę sylab w słowie? Jeśli to drugie, rozważ wyszukanie słów w słowniku zamiany tekstu na mowę i policz fonemy, które kodują dźwięki samogłosek.
Adrian McCarthy
Wydaje mi się, że najbardziej wydajnym sposobem (pod względem obliczeniowym; nie pod względem pamięci) byłoby po prostu mieć słownik Pythona ze słowami jako kluczami i liczbą sylab jako wartościami. Jednak nadal potrzebujesz rezerwy dla słów, które nie znalazły się w słowniku. Daj mi znać, jeśli kiedykolwiek znajdziesz taki słownik!
Brōtsyorfuzthrāx

Odpowiedzi:

120

Przeczytaj o podejściu TeX do tego problemu na potrzeby dzielenia wyrazów. Szczególnie patrz rozprawa doktorska Franka Lianga Word Hy-phen-a -ation autorstwa Com-put-er . Jego algorytm jest bardzo dokładny, a następnie zawiera mały słownik wyjątków dla przypadków, w których algorytm nie działa.

Jason
źródło
52
Podoba mi się, że zacytowałeś pracę dyplomową na ten temat, to mała wskazówka do oryginalnego plakatu, że może to nie być łatwe pytanie.
Karl
Tak, zdaję sobie sprawę, że nie jest to proste pytanie, chociaż niewiele nad tym pracowałem. Jednak nie doceniłem problemu, pomyślałem, że popracuję nad innymi częściami mojej aplikacji, a później wrócę do tego „prostego” problemu. Silly me :)
user50705
Przeczytałem rozprawę i uznałem ją za bardzo pomocną. Problem z tym podejściem polegał na tym, że nie miałem żadnych wzorców dla języka albańskiego, chociaż znalazłem narzędzia, które mogłyby je wygenerować. W każdym razie, dla swoich celów napisałem aplikację opartą na regułach, która rozwiązała problem ...
user50705
10
Zwróć uwagę, że algorytm TeX służy do znajdowania poprawnych punktów dzielenia wyrazów, co nie jest dokładnie tym samym, co podział na sylaby. Prawdą jest, że punkty podziału na sylaby przypadają na podział na sylaby, ale nie wszystkie podziały na sylaby są prawidłowymi punktami podziału. Na przykład łączniki nie są (zwykle) używane w jednej lub dwóch literach na każdym końcu słowa. Uważam również, że wzorce TeX-a zostały dostrojone tak, aby zamieniać fałszywe negatywy na fałszywe alarmy (nigdy nie umieszczaj myślnika tam, gdzie nie należy, nawet jeśli oznacza to utratę niektórych uzasadnionych możliwości dzielenia wyrazów).
Adrian McCarthy
1
Nie wierzę też, że dzielenie wyrazów jest odpowiedzią.
Ezequiel
46

Natknąłem się na tę stronę, szukając tego samego, i znalazłem kilka implementacji artykułu Liang tutaj: https://github.com/mnater/hyphenator lub następca: https://github.com/mnater/Hyphenopoly

Chyba że jesteś typem, który lubi czytać 60-stronicową pracę dyplomową zamiast dostosowywać swobodnie dostępny kod do nieunikalnego problemu. :)

Sean
źródło
zgodził się - o wiele wygodniej jest po prostu użyć istniejącej
implementacji
41

Oto rozwiązanie wykorzystujące NLTK :

from nltk.corpus import cmudict
d = cmudict.dict()
def nsyl(word):
  return [len(list(y for y in x if y[-1].isdigit())) for x in d[word.lower()]] 
hoju
źródło
Hej, dzięki maleńkiemu dziecku błąd w funkcji def nsyl (słowo): return [len (list (y for y in x if y [-1] .isdigit ())) for x in d [word.lower ()] ]
Gourneau,
6
Co byś zasugerował jako rezerwę dla słów, których nie ma w tym korpusie?
Dan Gayle
4
@Pureferret cmudict to słownik wymowy angielskich słów z Ameryki Północnej . dzieli słowa na fonemy, które są krótsze niż sylaby (np. słowo „kot” jest podzielone na trzy fonemy: K - AE - T). ale samogłoski mają również „znacznik stresu”: 0, 1 lub 2, w zależności od wymowy słowa (więc AE w „kot” staje się AE1). kod w odpowiedzi zlicza znaczniki akcentu, a tym samym liczbę samogłosek - co w efekcie daje liczbę sylab (zauważ, że w przykładach OP każda sylaba ma dokładnie jedną samogłoskę).
billy_chapters
1
Zwraca liczbę sylab, a nie sylaby.
Adam Michael Wood
19

Próbuję rozwiązać ten problem w programie, który obliczy wynik czytania flesch-kincaid i flesch bloku tekstu. Mój algorytm wykorzystuje to, co znalazłem na tej stronie: http://www.howmanysyllables.com/howtocountsyllables.html i zbliża się do siebie. Nadal ma problemy ze skomplikowanymi słowami, takimi jak niewidoczne i dzielenie wyrazów, ale odkryłem, że trafia do moich celów.

Ma tę zaletę, że jest łatwy do wdrożenia. Odkryłem, że „es” może być sylabiczne lub nie. To ryzykowne, ale zdecydowałem się usunąć es z mojego algorytmu.

private int CountSyllables(string word)
    {
        char[] vowels = { 'a', 'e', 'i', 'o', 'u', 'y' };
        string currentWord = word;
        int numVowels = 0;
        bool lastWasVowel = false;
        foreach (char wc in currentWord)
        {
            bool foundVowel = false;
            foreach (char v in vowels)
            {
                //don't count diphthongs
                if (v == wc && lastWasVowel)
                {
                    foundVowel = true;
                    lastWasVowel = true;
                    break;
                }
                else if (v == wc && !lastWasVowel)
                {
                    numVowels++;
                    foundVowel = true;
                    lastWasVowel = true;
                    break;
                }
            }

            //if full cycle and no vowel found, set lastWasVowel to false;
            if (!foundVowel)
                lastWasVowel = false;
        }
        //remove es, it's _usually? silent
        if (currentWord.Length > 2 && 
            currentWord.Substring(currentWord.Length - 2) == "es")
            numVowels--;
        // remove silent e
        else if (currentWord.Length > 1 &&
            currentWord.Substring(currentWord.Length - 1) == "e")
            numVowels--;

        return numVowels;
    }
Joe Basirico
źródło
Dla mojego prostego scenariusza znajdowania sylab w nazwach własnych wydaje się to początkowo działać wystarczająco dobrze. Dzięki, że to tutaj umieściłeś.
Norman H
5

Po co to obliczać? Każdy słownik online zawiera te informacje. http://dictionary.reference.com/browse/invisible in · vis · i · ble

Cerin
źródło
3
Może musi działać w przypadku słów, które nie pojawiają się w słownikach, takich jak imiona?
Wouter Lievens
4
@WouterLievens: Nie sądzę, żeby imiona były wystarczająco grzeczne, aby umożliwić automatyczne analizowanie sylab. Parser sylab dla angielskich nazw zawiódłby żałośnie w przypadku nazw pochodzenia walijskiego lub szkockiego, nie mówiąc już o nazwach pochodzenia indyjskiego i nigeryjskiego, ale możesz znaleźć je wszystkie w jednym pokoju, gdzieś np. W Londynie.
Jean-François Corbett
Należy pamiętać, że nie jest rozsądne oczekiwanie lepszych wyników, niż mógłby zapewnić człowiek, biorąc pod uwagę, że jest to czysto heurystyczne podejście do szkicowej dziedziny.
Darren Ringer
5

Dzięki Joe Basirico za udostępnienie Twojej szybkiej i brudnej implementacji w C #. Korzystałem z dużych bibliotek i działają, ale zwykle są trochę powolne, a w przypadku szybkich projektów twoja metoda działa dobrze.

Oto Twój kod w Javie wraz z przypadkami testowymi:

public static int countSyllables(String word)
{
    char[] vowels = { 'a', 'e', 'i', 'o', 'u', 'y' };
    char[] currentWord = word.toCharArray();
    int numVowels = 0;
    boolean lastWasVowel = false;
    for (char wc : currentWord) {
        boolean foundVowel = false;
        for (char v : vowels)
        {
            //don't count diphthongs
            if ((v == wc) && lastWasVowel)
            {
                foundVowel = true;
                lastWasVowel = true;
                break;
            }
            else if (v == wc && !lastWasVowel)
            {
                numVowels++;
                foundVowel = true;
                lastWasVowel = true;
                break;
            }
        }
        // If full cycle and no vowel found, set lastWasVowel to false;
        if (!foundVowel)
            lastWasVowel = false;
    }
    // Remove es, it's _usually? silent
    if (word.length() > 2 && 
            word.substring(word.length() - 2) == "es")
        numVowels--;
    // remove silent e
    else if (word.length() > 1 &&
            word.substring(word.length() - 1) == "e")
        numVowels--;
    return numVowels;
}

public static void main(String[] args) {
    String txt = "what";
    System.out.println("txt="+txt+" countSyllables="+countSyllables(txt));
    txt = "super";
    System.out.println("txt="+txt+" countSyllables="+countSyllables(txt));
    txt = "Maryland";
    System.out.println("txt="+txt+" countSyllables="+countSyllables(txt));
    txt = "American";
    System.out.println("txt="+txt+" countSyllables="+countSyllables(txt));
    txt = "disenfranchized";
    System.out.println("txt="+txt+" countSyllables="+countSyllables(txt));
    txt = "Sophia";
    System.out.println("txt="+txt+" countSyllables="+countSyllables(txt));
}

Wynik był zgodny z oczekiwaniami (działa wystarczająco dobrze dla Flesch-Kincaid):

txt=what countSyllables=1
txt=super countSyllables=2
txt=Maryland countSyllables=3
txt=American countSyllables=3
txt=disenfranchized countSyllables=5
txt=Sophia countSyllables=2
Tihamer
źródło
5

Bumping @Tihamer i @ joe-basirico. Bardzo przydatna funkcja, nie doskonała , ale dobra dla większości małych i średnich projektów. Joe, przepisałem implementację twojego kodu w Pythonie:

def countSyllables(word):
    vowels = "aeiouy"
    numVowels = 0
    lastWasVowel = False
    for wc in word:
        foundVowel = False
        for v in vowels:
            if v == wc:
                if not lastWasVowel: numVowels+=1   #don't count diphthongs
                foundVowel = lastWasVowel = True
                        break
        if not foundVowel:  #If full cycle and no vowel found, set lastWasVowel to false
            lastWasVowel = False
    if len(word) > 2 and word[-2:] == "es": #Remove es - it's "usually" silent (?)
        numVowels-=1
    elif len(word) > 1 and word[-1:] == "e":    #remove silent e
        numVowels-=1
    return numVowels

Mam nadzieję, że ktoś uzna to za przydatne!

Tersozaury
źródło
4

Perl posiada moduł Lingua :: Phonology :: Syllable . Możesz tego spróbować lub przyjrzeć się jego algorytmowi. Widziałem tam też kilka innych starszych modułów.

Nie rozumiem, dlaczego wyrażenie regularne podaje tylko liczbę sylab. Powinieneś być w stanie pobrać same sylaby za pomocą nawiasów przechwytujących. Zakładając, że możesz skonstruować wyrażenie regularne, które działa, to znaczy.

skiphoppy
źródło
4

Dzisiaj znalazłem implementację w Javie algorytmu dzielenia wyrazów Franka Lianga ze wzorcem dla języka angielskiego lub niemieckiego, który działa całkiem dobrze i jest dostępny w Maven Central.

Jaskinia: Ważne jest, aby usunąć ostatnie wiersze .texplików sygnatur, ponieważ w przeciwnym razie te pliki nie mogą zostać załadowane z aktualną wersją na Maven Central.

Aby załadować i używać hyphenator, możesz użyć następującego fragmentu kodu Java. texTableto nazwa .texplików zawierających potrzebne wzorce. Pliki te są dostępne na stronie projektu github.

 private Hyphenator createHyphenator(String texTable) {
        Hyphenator hyphenator = new Hyphenator();
        hyphenator.setErrorHandler(new ErrorHandler() {
            public void debug(String guard, String s) {
                logger.debug("{},{}", guard, s);
            }

            public void info(String s) {
                logger.info(s);
            }

            public void warning(String s) {
                logger.warn("WARNING: " + s);
            }

            public void error(String s) {
                logger.error("ERROR: " + s);
            }

            public void exception(String s, Exception e) {
                logger.error("EXCEPTION: " + s, e);
            }

            public boolean isDebugged(String guard) {
                return false;
            }
        });

        BufferedReader table = null;

        try {
            table = new BufferedReader(new InputStreamReader(Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream((texTable)), Charset.forName("UTF-8")));
            hyphenator.loadTable(table);
        } catch (Utf8TexParser.TexParserException e) {
            logger.error("error loading hyphenation table: {}", e.getLocalizedMessage(), e);
            throw new RuntimeException("Failed to load hyphenation table", e);
        } finally {
            if (table != null) {
                try {
                    table.close();
                } catch (IOException e) {
                    logger.error("Closing hyphenation table failed", e);
                }
            }
        }

        return hyphenator;
    }

Następnie Hyphenatorjest gotowy do użycia. Aby wykryć sylaby, podstawową ideą jest podzielenie terminu na podane łączniki.

    String hyphenedTerm = hyphenator.hyphenate(term);

    String hyphens[] = hyphenedTerm.split("\u00AD");

    int syllables = hyphens.length;

Musisz podzielić na "\u00AD”, ponieważ API nie zwraca normalnego "-".

Takie podejście przewyższa odpowiedź Joe Basirico, ponieważ obsługuje wiele różnych języków i wykrywa dokładniejsze dzielenie wyrazów w języku niemieckim.

rzo
źródło
4

Niedawno natknąłem się na ten sam problem.

Skończyło się na używaniu słownika wymowy CMU do szybkiego i dokładnego wyszukiwania większości słów. W przypadku słów, których nie ma w słowniku, powróciłem do modelu uczenia maszynowego, który jest w ~ 98% dokładny w przewidywaniu liczby sylab.

Całość zawarłem w łatwym w użyciu module Python tutaj: https://github.com/repp/big-phoney

Zainstalować: pip install big-phoney

Policz sylaby:

from big_phoney import BigPhoney
phoney = BigPhoney()
phoney.count_syllables('triceratops')  # --> 4

Jeśli nie używasz Pythona i chcesz wypróbować podejście oparte na modelu ML, napisałem dość szczegółowo , jak działa model liczenia sylab w Kaggle .

Ryan Epp
źródło
To jest super fajne. Czy ktoś miał szczęście przekonwertować wynikowy model Keras na model CoreML do użytku na iOS?
Alexsander Akers
2

Dziękuję @ joe-basirico i @tihamer. Przeportowałem kod @ tihamera do Lua 5.1, 5.2 i luajit 2 ( najprawdopodobniej będzie działać również na innych wersjach lua ):

countsyllables.lua

function CountSyllables(word)
  local vowels = { 'a','e','i','o','u','y' }
  local numVowels = 0
  local lastWasVowel = false

  for i = 1, #word do
    local wc = string.sub(word,i,i)
    local foundVowel = false;
    for _,v in pairs(vowels) do
      if (v == string.lower(wc) and lastWasVowel) then
        foundVowel = true
        lastWasVowel = true
      elseif (v == string.lower(wc) and not lastWasVowel) then
        numVowels = numVowels + 1
        foundVowel = true
        lastWasVowel = true
      end
    end

    if not foundVowel then
      lastWasVowel = false
    end
  end

  if string.len(word) > 2 and
    string.sub(word,string.len(word) - 1) == "es" then
    numVowels = numVowels - 1
  elseif string.len(word) > 1 and
    string.sub(word,string.len(word)) == "e" then
    numVowels = numVowels - 1
  end

  return numVowels
end

I kilka zabawnych testów, aby potwierdzić, że działa ( tak bardzo, jak powinno ):

countsyllables.tests.lua

require "countsyllables"

tests = {
  { word = "what", syll = 1 },
  { word = "super", syll = 2 },
  { word = "Maryland", syll = 3},
  { word = "American", syll = 4},
  { word = "disenfranchized", syll = 5},
  { word = "Sophia", syll = 2},
  { word = "End", syll = 1},
  { word = "I", syll = 1},
  { word = "release", syll = 2},
  { word = "same", syll = 1},
}

for _,test in pairs(tests) do
  local resultSyll = CountSyllables(test.word)
  assert(resultSyll == test.syll,
    "Word: "..test.word.."\n"..
    "Expected: "..test.syll.."\n"..
    "Result: "..resultSyll)
end

print("Tests passed.")
josefnpat
źródło
Dodałem jeszcze dwa przypadki testowe „End” i „I”. Rozwiązaniem było niewrażliwe porównywanie wielkości liter w łańcuchach. Ping'ing @ joe-basirico i tihamer na wypadek, gdyby mieli ten sam problem i chcieliby zaktualizować swoje funkcje.
josefnpat
@tihamer American to 4 sylaby!
josefnpat
2

Nie mogłem znaleźć odpowiedniego sposobu liczenia sylab, więc sam zaprojektowałem metodę.

Możesz zobaczyć moją metodę tutaj: https://stackoverflow.com/a/32784041/2734752

Do liczenia sylab używam kombinacji słownika i algorytmu.

Możesz wyświetlić moją bibliotekę tutaj: https://github.com/troywatson/Lawrence-Style-Checker

Właśnie przetestowałem mój algorytm i uzyskałem 99,4% skuteczności!

Lawrence lawrence = new Lawrence();

System.out.println(lawrence.getSyllable("hyphenation"));
System.out.println(lawrence.getSyllable("computer"));

Wynik:

4
3
Troy
źródło
1
Ogólnie rzecz biorąc, odsyłaczom do narzędzia lub biblioteki powinny towarzyszyć uwagi dotyczące użytkowania, szczegółowe wyjaśnienie, w jaki sposób połączony zasób można zastosować do problemu lub przykładowy kod lub, jeśli to możliwe, wszystkie powyższe.
IKavanagh
Zobacz Podświetlanie składni . W edytorze SO znajduje się przycisk pomocy (znak zapytania), który prowadzi do połączonej strony.
IKavanagh
0

Po wielu testach i wypróbowaniu pakietów dzielenia wyrazów, napisałem swój własny na podstawie kilku przykładów. Wypróbowałem również pakiety pyhypheni pyphen, które łączą się ze słownikami dzielenia wyrazów, ale w wielu przypadkach generują one niewłaściwą liczbę sylab. nltkPakiet był po prostu zbyt powolne dla tego przypadku użycia.

Moja implementacja w Pythonie jest częścią klasy, którą napisałem, a procedura liczenia sylab jest wklejona poniżej. Trochę zawyża liczbę sylab, ponieważ nadal nie znalazłem dobrego sposobu na wyjaśnienie cichych zakończeń słów.

Funkcja zwraca stosunek sylab na słowo, tak jak jest używana do oceny czytelności Flescha-Kincaida. Liczba nie musi być dokładna, wystarczy, że jest na tyle bliska, aby można było ją oszacować.

Na moim procesorze i7 7. generacji ta funkcja zajęła 1,1-1,2 milisekundy dla przykładowego tekstu o długości 759 słów.

def _countSyllablesEN(self, theText):

    cleanText = ""
    for ch in theText:
        if ch in "abcdefghijklmnopqrstuvwxyz'’":
            cleanText += ch
        else:
            cleanText += " "

    asVow    = "aeiouy'’"
    dExep    = ("ei","ie","ua","ia","eo")
    theWords = cleanText.lower().split()
    allSylls = 0
    for inWord in theWords:
        nChar  = len(inWord)
        nSyll  = 0
        wasVow = False
        wasY   = False
        if nChar == 0:
            continue
        if inWord[0] in asVow:
            nSyll += 1
            wasVow = True
            wasY   = inWord[0] == "y"
        for c in range(1,nChar):
            isVow  = False
            if inWord[c] in asVow:
                nSyll += 1
                isVow = True
            if isVow and wasVow:
                nSyll -= 1
            if isVow and wasY:
                nSyll -= 1
            if inWord[c:c+2] in dExep:
                nSyll += 1
            wasVow = isVow
            wasY   = inWord[c] == "y"
        if inWord.endswith(("e")):
            nSyll -= 1
        if inWord.endswith(("le","ea","io")):
            nSyll += 1
        if nSyll < 1:
            nSyll = 1
        # print("%-15s: %d" % (inWord,nSyll))
        allSylls += nSyll

    return allSylls/len(theWords)
Jadzia626
źródło
-1

Kiedyś użyłem do tego jsoup. Oto przykładowy parser sylaby:

public String[] syllables(String text){
        String url = "https://www.merriam-webster.com/dictionary/" + text;
        String relHref;
        try{
            Document doc = Jsoup.connect(url).get();
            Element link = doc.getElementsByClass("word-syllables").first();
            if(link == null){return new String[]{text};}
            relHref = link.html(); 
        }catch(IOException e){
            relHref = text;
        }
        String[] syl = relHref.split("·");
        return syl;
    }
Itamar Fiorino
źródło
Jak to jest generyczny parser sylaby? Wygląda na to, że ten kod wyszukuje tylko sylaby w słowniku
Nico Haase