Come dividere il testo senza spazi in un elenco di parole?


106

Ingresso: "tableapplechairtablecupboard..." molte parole

Quale sarebbe un algoritmo efficiente per suddividere tale testo nell'elenco di parole e ottenere:

Produzione: ["table", "apple", "chair", "table", ["cupboard", ["cup", "board"]], ...]

La prima cosa che mi viene in mente è esaminare tutte le parole possibili (iniziando con la prima lettera) e trovare la parola più lunga possibile, continua da position=word_position+len(word)

PS
Abbiamo un elenco di tutte le parole possibili.
La parola "armadio" può essere "tazza" e "tavola", selezionare la più lunga.
Linguaggio: python, ma la cosa principale è l'algoritmo stesso.


14
Sei sicuro che la stringa non inizi con le parole "tab" e "leap"?
Rob Hruska

Sì, sembra che non possa essere fatto in modo univoco.
demalexx

@ RobHruska, in quel caso ho scritto, selezionando il più lungo possibile.
Sergey

2
@Sergey - Il tuo criterio "più lungo possibile" implicava che fosse per parole composte. E in quel caso, cosa succederebbe se la corda fosse "carpetrel". Sarebbe "tappeto" o "petrel"?
Rob Hruska

2
Ci sono molte parole di dettatura nella tua stringa:['able', 'air', 'apple', 'boa', 'boar', 'board', 'chair', 'cup', 'cupboard', 'ha', 'hair', 'lea', 'leap', 'oar', 'tab', 'table', 'up']
reclosedev

Risposte:


200

Un algoritmo ingenuo non darà buoni risultati se applicato a dati del mondo reale. Ecco un algoritmo di 20 righe che sfrutta la frequenza relativa delle parole per fornire risultati accurati per il testo con parole reali.

(Se vuoi una risposta alla tua domanda originale che non utilizza la frequenza delle parole, devi perfezionare cosa si intende esattamente per "parola più lunga": è meglio avere una parola di 20 lettere e dieci parole di 3 lettere, o è è meglio avere cinque parole di 10 lettere? Una volta stabilita una definizione precisa, non ti resta che cambiare la linea che definisce wordcost per riflettere il significato previsto.)

L'idea

Il modo migliore per procedere è modellare la distribuzione dell'output. Una buona prima approssimazione è assumere che tutte le parole siano distribuite indipendentemente. Quindi devi solo conoscere la frequenza relativa di tutte le parole. È ragionevole presumere che seguano la legge di Zipf, ovvero la parola con rango n nell'elenco di parole ha probabilità all'incirca 1 / ( n log N ) dove N è il numero di parole nel dizionario.

Dopo aver fissato il modello, è possibile utilizzare la programmazione dinamica per dedurre la posizione degli spazi. La frase più probabile è quella che massimizza il prodotto della probabilità di ogni singola parola, ed è facile calcolarla con la programmazione dinamica. Invece di usare direttamente la probabilità utilizziamo un costo definito come il logaritmo dell'inverso della probabilità per evitare overflow.

Il codice

from math import log

# Build a cost dictionary, assuming Zipf's law and cost = -math.log(probability).
words = open("words-by-frequency.txt").read().split()
wordcost = dict((k, log((i+1)*log(len(words)))) for i,k in enumerate(words))
maxword = max(len(x) for x in words)

def infer_spaces(s):
    """Uses dynamic programming to infer the location of spaces in a string
    without spaces."""

    # Find the best match for the i first characters, assuming cost has
    # been built for the i-1 first characters.
    # Returns a pair (match_cost, match_length).
    def best_match(i):
        candidates = enumerate(reversed(cost[max(0, i-maxword):i]))
        return min((c + wordcost.get(s[i-k-1:i], 9e999), k+1) for k,c in candidates)

    # Build the cost array.
    cost = [0]
    for i in range(1,len(s)+1):
        c,k = best_match(i)
        cost.append(c)

    # Backtrack to recover the minimal-cost string.
    out = []
    i = len(s)
    while i>0:
        c,k = best_match(i)
        assert c == cost[i]
        out.append(s[i-k:i])
        i -= k

    return " ".join(reversed(out))

con cui puoi usare

s = 'thumbgreenappleactiveassignmentweeklymetaphor'
print(infer_spaces(s))

I risultati

Sto usando questo dizionario veloce e sporco di 125k parole che ho messo insieme da un piccolo sottoinsieme di Wikipedia.

Prima: thumbgreenappleactiveassignmentweeklymetaphor.
Dopo: metafora settimanale dell'assegnazione attiva della mela verde del pollice.

Prima: informazioni di testoinformazionidellopersecommenti che sono stati visualizzati da htmlbuttherearen odelimitedcharacterin themforexamplethumbgreenappleactiveassignmentweeklymetapho rapparentlytherearethumbgreenappleetcinthestringialsohavealargedictionarytoquery ifthewordshereafastexway.

Dopo: ci sono masse di informazioni di testo dei commenti delle persone che vengono analizzate da html ma non ci sono caratteri delimitati in essi, ad esempio pollice mela verde assegnazione attiva metafora settimanale apparentemente ci sono pollice mela verde ecc.Nella stringa ho anche un grande dizionario per chiedi se la parola è ragionevole, quindi qual è il modo più veloce di estrarre grazie.

Prima: buio e tempesta per la notte in cui cadde il torrenteccetto ad intervalli occasionali quando veniva controllato da un'avvolgente raffica di vento che ha spazzato le strade per le strade della foresta inondata da picchiaduro.

Dopo: era una notte buia e tempestosa la pioggia cadeva a torrenti tranne che a intervalli occasionali quando veniva frenata da una violenta folata di vento che spazzava le strade perché è a Londra che la nostra scena giace tintinnando lungo i tetti e agitando ferocemente il scarsa fiamma delle lampade che lottavano contro l'oscurità.

Come puoi vedere è essenzialmente impeccabile. La parte più importante è assicurarti che il tuo elenco di parole sia stato addestrato a un corpus simile a quello che incontrerai effettivamente, altrimenti i risultati saranno pessimi.


Ottimizzazione

L'implementazione consuma una quantità lineare di tempo e memoria, quindi è ragionevolmente efficiente. Se hai bisogno di ulteriori accelerazioni, puoi creare un albero dei suffissi dall'elenco di parole per ridurre la dimensione del set di candidati.

Se è necessario elaborare una stringa consecutiva molto grande, sarebbe ragionevole dividere la stringa per evitare un utilizzo eccessivo della memoria. Ad esempio, è possibile elaborare il testo in blocchi di 10000 caratteri più un margine di 1000 caratteri su entrambi i lati per evitare effetti di confine. Ciò manterrà l'utilizzo della memoria al minimo e quasi certamente non avrà alcun effetto sulla qualità.


1
che dire di due righe di testo?
partenza il

11
Questo codice mi ha reso insensibile. Non ho capito un po '. Non capisco le cose del registro. Ma ho testato questo codice sul mio computer. Sei un genio.
Aditya Singh

1
Qual è il tempo di esecuzione di questo algoritmo? Perché non usi ahocorasick?
RetroCode

8
Questo è eccellente. L'ho trasformato in un pacchetto pip: pypi.python.org/pypi/wordninja pip install wordninja
keredson

2
@wittrup tuo words.txt contiene "comp": `` `$ grep" ^ comp $ "words.txt comp` `` ed è ordinato alfabeticamente. questo codice presume che sia ordinato in base alla frequenza di apparizione decrescente (cosa comune per elenchi di n-grammi come questo). se usi un elenco ordinato correttamente, la tua stringa esce bene: `` >>> wordninja.split ('namethecompanywherebonniewasemployedwhenwestarteddating') ['name', 'the', 'company', 'where', 'bonnie', ' era ',' impiegato ',' quando ',' noi ',' iniziato ',' incontri '] ``
keredson,

50

Sulla base dell'ottimo lavoro nella risposta in alto , ho creato un pippacchetto per un facile utilizzo.

>>> import wordninja
>>> wordninja.split('derekanderson')
['derek', 'anderson']

Per installare, esegui pip install wordninja .

Le uniche differenze sono minori. Questo restituisce a listpiuttosto che a str, funziona inpython3 , include l'elenco di parole e si divide correttamente anche se ci sono caratteri non alfabetici (come trattini bassi, trattini, ecc.)

Grazie ancora a Generic Human!

https://github.com/keredson/wordninja


2
Grazie per aver creato questo.
Mohit Bhatia

1
Grazie! Adoro il fatto che tu ne abbia fatto un pacchetto. Il metodo sottostante non ha funzionato molto bene per me. Ad esempio, i "lettini" sono stati suddivisi in "lounge" e "rs"
Harry M

@keredson - Prima di tutto, grazie per la soluzione. Si comporta bene. Tuttavia, rimuove i caratteri speciali come "-" ecc. A volte non fornisce la divisione corretta come prendere una stringa lunga dire - "WeatheringPropertiesbyMaterial Trade Name Graph 2-1. Cambiamento di colore, E, dopo Arizona, Florida, Cycolac® / Sistemi di resina Geloy® rispetto al PVC. [15] 25 20 15 ∆E 10 5 0 PVC, PVC bianco, Brown C / G, BrownC / G. Capstock è il materiale utilizzato come strato superficiale applicato alla superficie esterna di un profilo estrusione. Capstock in resina Geloy® su un substrato Cycolac® fornisce un'eccezionale resistenza agli agenti atmosferici. [25] "
Pila di lampade Rakesh,

puoi aprire un problema in GH?
keredson

1
Bel lavoro, grazie per lo sforzo. Mi ha davvero fatto risparmiare un sacco di tempo.
Jan Zeiseweis

17

Ecco la soluzione che utilizza la ricerca ricorsiva:

def find_words(instring, prefix = '', words = None):
    if not instring:
        return []
    if words is None:
        words = set()
        with open('/usr/share/dict/words') as f:
            for line in f:
                words.add(line.strip())
    if (not prefix) and (instring in words):
        return [instring]
    prefix, suffix = prefix + instring[0], instring[1:]
    solutions = []
    # Case 1: prefix in solution
    if prefix in words:
        try:
            solutions.append([prefix] + find_words(suffix, '', words))
        except ValueError:
            pass
    # Case 2: prefix not in solution
    try:
        solutions.append(find_words(suffix, prefix, words))
    except ValueError:
        pass
    if solutions:
        return sorted(solutions,
                      key = lambda solution: [len(word) for word in solution],
                      reverse = True)[0]
    else:
        raise ValueError('no solution')

print(find_words('tableapplechairtablecupboard'))
print(find_words('tableprechaun', words = set(['tab', 'table', 'leprechaun'])))

i rendimenti

['table', 'apple', 'chair', 'table', 'cupboard']
['tab', 'leprechaun']

funziona "fuori dagli schemi", grazie! Penso anche di usare la struttura trie come ha detto miku, non solo l'insieme di tutte le parole. Grazie comunque!
Sergey

11

Utilizzando una struttura dati trie , che contiene l'elenco delle possibili parole, non sarebbe troppo complicato fare quanto segue:

  1. Puntatore di avanzamento (nella stringa concatenata)
  2. Cerca e archivia il nodo corrispondente nel trie
  3. Se il nodo trie ha figli (ad es. Ci sono parole più lunghe), vai a 1.
  4. Se il nodo raggiunto non ha figli, si è verificata una corrispondenza di parole più lunga; aggiungi la parola (memorizzata nel nodo o semplicemente concatenata durante l'attraversamento del trie) all'elenco dei risultati, reimposta il puntatore nel trie (o reimposta il riferimento) e ricomincia

3
Se l'obiettivo è consumare l'intera stringa, è necessario tornare indietro, "tableprechaun"quindi dividere dopo "tab".
Daniel Fischer

In più per aver menzionato trie, ma sono anche d'accordo con Daniel, che il backtracking deve essere fatto.
Sergey

@Daniel, la ricerca della corrispondenza più lunga non necessita di backtracking, no. Cosa te lo fa pensare? E cosa c'è di sbagliato con l'algoritmo di cui sopra?
Devin Jeanpierre

1
@Devin Il fatto che per "tableprechaun"la partita più lunga dall'inizio è "table", in partenza "prechaun", che non può essere suddiviso in parole del dizionario. Quindi devi prendere la partita più breve "tab"lasciandoti con un "leprechaun".
Daniel Fischer

@ Daniel, scusa, sì. Ho frainteso il problema. L'algoritmo corretto dovrebbe tenere traccia di tutte le possibili posizioni degli alberi contemporaneamente: ricerca NFA in tempo lineare AKA. Oppure tornare indietro, certo, ma è il momento esponenziale nel peggiore dei casi.
Devin Jeanpierre

9

La soluzione di Unutbu era abbastanza vicina ma trovo il codice difficile da leggere e non ha prodotto il risultato atteso. La soluzione di Generic Human ha l'inconveniente di aver bisogno delle frequenze delle parole. Non appropriato per tutti i casi d'uso.

Ecco una semplice soluzione utilizzando un algoritmo Divide and Conquer .

  1. Cerca di ridurre al minimo il numero di parole che Eg find_words('cupboard')restituirà ['cupboard']anziché ['cup', 'board'](supponendo che cupboard, cupeboard siano nel dizionario)
  2. La soluzione ottimale non è univoca , l'implementazione di seguito restituisce una soluzione. find_words('charactersin')potrebbe tornare ['characters', 'in']o forse tornerà ['character', 'sin'](come visto sotto). Si potrebbe facilmente modificare l'algoritmo per restituire tutte le soluzioni ottimali.
  3. In questa implementazione le soluzioni vengono memorizzate in modo che vengano eseguite in un tempo ragionevole.

Il codice:

words = set()
with open('/usr/share/dict/words') as f:
    for line in f:
        words.add(line.strip())

solutions = {}
def find_words(instring):
    # First check if instring is in the dictionnary
    if instring in words:
        return [instring]
    # No... But maybe it's a result we already computed
    if instring in solutions:
        return solutions[instring]
    # Nope. Try to split the string at all position to recursively search for results
    best_solution = None
    for i in range(1, len(instring) - 1):
        part1 = find_words(instring[:i])
        part2 = find_words(instring[i:])
        # Both parts MUST have a solution
        if part1 is None or part2 is None:
            continue
        solution = part1 + part2
        # Is the solution found "better" than the previous one?
        if best_solution is None or len(solution) < len(best_solution):
            best_solution = solution
    # Remember (memoize) this solution to avoid having to recompute it
    solutions[instring] = best_solution
    return best_solution

Questo richiederà circa 5 secondi sulla mia macchina a 3GHz:

result = find_words("thereismassesoftextinformationofpeoplescommentswhichisparsedfromhtmlbuttherearenodelimitedcharactersinthemforexamplethumbgreenappleactiveassignmentweeklymetaphorapparentlytherearethumbgreenappleetcinthestringialsohavealargedictionarytoquerywhetherthewordisreasonablesowhatsthefastestwayofextractionthxalot")
assert(result is not None)
print ' '.join(result)

le masse di informazioni di testo dei commenti delle persone che vengono analizzate da html ma non ci sono caratteri delimitati peccateli per esempio pollice mela verde assegnazione attiva metafora settimanale apparentemente ci sono pollice mela verde ecc. nella stringa ho anche un grande dizionario per chiedere se la parola è ragionevole, quindi qual è il modo più veloce di estrazione grazie a molti


Non c'è motivo di credere che un testo non possa terminare con una parola di una sola lettera. Dovresti considerare una divisione in più.
panda-34

7

La risposta di https://stackoverflow.com/users/1515832/generic-human è fantastica. Ma la migliore implementazione di questo che abbia mai visto è stata scritta dallo stesso Peter Norvig nel suo libro "Beautiful Data".

Prima di incollare il suo codice, lasciatemi spiegare perché il metodo di Norvig è più accurato (anche se un po 'più lento e più lungo in termini di codice).

1) I dati sono un po 'migliori, sia in termini di dimensioni che in termini di precisione (usa un conteggio di parole piuttosto che una semplice classifica) 2) Ancora più importante, è la logica dietro n-grammi che rende l'approccio così accurato .

L'esempio che fornisce nel suo libro è il problema della divisione di una stringa "sitdown". Ora un metodo di suddivisione delle stringhe non Bigram prenderebbe in considerazione p ('sit') * p ('down'), e se questo è inferiore a p ('sitdown') - che sarà il caso abbastanza spesso - NON verrà diviso esso, ma lo vorremmo (la maggior parte delle volte).

Tuttavia, quando hai il modello bigram potresti valutare p ('sit down') come bigram vs p ('sitdown') e il primo vince. Fondamentalmente, se non usi bigram, tratta la probabilità delle parole che stai dividendo come indipendente, il che non è il caso, alcune parole hanno maggiori probabilità di apparire una dopo l'altra. Sfortunatamente quelle sono anche le parole che spesso sono incollate insieme in molti casi e confondono lo splitter.

Ecco il collegamento ai dati (sono i dati per 3 problemi separati e la segmentazione è solo uno. Si prega di leggere il capitolo per i dettagli): http://norvig.com/ngrams/

ed ecco il link al codice: http://norvig.com/ngrams/ngrams.py

Questi collegamenti sono attivi da un po ', ma copierò comunque la parte di segmentazione del codice qui

import re, string, random, glob, operator, heapq
from collections import defaultdict
from math import log10

def memo(f):
    "Memoize function f."
    table = {}
    def fmemo(*args):
        if args not in table:
            table[args] = f(*args)
        return table[args]
    fmemo.memo = table
    return fmemo

def test(verbose=None):
    """Run some tests, taken from the chapter.
    Since the hillclimbing algorithm is randomized, some tests may fail."""
    import doctest
    print 'Running tests...'
    doctest.testfile('ngrams-test.txt', verbose=verbose)

################ Word Segmentation (p. 223)

@memo
def segment(text):
    "Return a list of words that is the best segmentation of text."
    if not text: return []
    candidates = ([first]+segment(rem) for first,rem in splits(text))
    return max(candidates, key=Pwords)

def splits(text, L=20):
    "Return a list of all possible (first, rem) pairs, len(first)<=L."
    return [(text[:i+1], text[i+1:]) 
            for i in range(min(len(text), L))]

def Pwords(words): 
    "The Naive Bayes probability of a sequence of words."
    return product(Pw(w) for w in words)

#### Support functions (p. 224)

def product(nums):
    "Return the product of a sequence of numbers."
    return reduce(operator.mul, nums, 1)

class Pdist(dict):
    "A probability distribution estimated from counts in datafile."
    def __init__(self, data=[], N=None, missingfn=None):
        for key,count in data:
            self[key] = self.get(key, 0) + int(count)
        self.N = float(N or sum(self.itervalues()))
        self.missingfn = missingfn or (lambda k, N: 1./N)
    def __call__(self, key): 
        if key in self: return self[key]/self.N  
        else: return self.missingfn(key, self.N)

def datafile(name, sep='\t'):
    "Read key,value pairs from file."
    for line in file(name):
        yield line.split(sep)

def avoid_long_words(key, N):
    "Estimate the probability of an unknown word."
    return 10./(N * 10**len(key))

N = 1024908267229 ## Number of tokens

Pw  = Pdist(datafile('count_1w.txt'), N, avoid_long_words)

#### segment2: second version, with bigram counts, (p. 226-227)

def cPw(word, prev):
    "Conditional probability of word, given previous word."
    try:
        return P2w[prev + ' ' + word]/float(Pw[prev])
    except KeyError:
        return Pw(word)

P2w = Pdist(datafile('count_2w.txt'), N)

@memo 
def segment2(text, prev='<S>'): 
    "Return (log P(words), words), where words is the best segmentation." 
    if not text: return 0.0, [] 
    candidates = [combine(log10(cPw(first, prev)), first, segment2(rem, first)) 
                  for first,rem in splits(text)] 
    return max(candidates) 

def combine(Pfirst, first, (Prem, rem)): 
    "Combine first and rem results into one (probability, words) pair." 
    return Pfirst+Prem, [first]+rem 

Funziona bene, ma quando provo ad applicarlo su tutto il mio set di dati, continua a direRuntimeError: maximum recursion depth exceeded in cmp
Harry M

ngrams ti darà sicuramente un aumento della precisione con un dict di frequenza esponenzialmente più grande, memoria e utilizzo del calcolo. btw la funzione memo sta perdendo memoria come un setaccio lì. dovrebbe cancellarlo tra le chiamate.
keredson

3

Ecco la risposta accettata tradotta in JavaScript (richiede node.js e il file "wordninja_words.txt" da https://github.com/keredson/wordninja ):

var fs = require("fs");

var splitRegex = new RegExp("[^a-zA-Z0-9']+", "g");
var maxWordLen = 0;
var wordCost = {};

fs.readFile("./wordninja_words.txt", 'utf8', function(err, data) {
    if (err) {
        throw err;
    }
    var words = data.split('\n');
    words.forEach(function(word, index) {
        wordCost[word] = Math.log((index + 1) * Math.log(words.length));
    })
    words.forEach(function(word) {
        if (word.length > maxWordLen)
            maxWordLen = word.length;
    });
    console.log(maxWordLen)
    splitRegex = new RegExp("[^a-zA-Z0-9']+", "g");
    console.log(split(process.argv[2]));
});


function split(s) {
    var list = [];
    s.split(splitRegex).forEach(function(sub) {
        _split(sub).forEach(function(word) {
            list.push(word);
        })
    })
    return list;
}
module.exports = split;


function _split(s) {
    var cost = [0];

    function best_match(i) {
        var candidates = cost.slice(Math.max(0, i - maxWordLen), i).reverse();
        var minPair = [Number.MAX_SAFE_INTEGER, 0];
        candidates.forEach(function(c, k) {
            if (wordCost[s.substring(i - k - 1, i).toLowerCase()]) {
                var ccost = c + wordCost[s.substring(i - k - 1, i).toLowerCase()];
            } else {
                var ccost = Number.MAX_SAFE_INTEGER;
            }
            if (ccost < minPair[0]) {
                minPair = [ccost, k + 1];
            }
        })
        return minPair;
    }

    for (var i = 1; i < s.length + 1; i++) {
        cost.push(best_match(i)[0]);
    }

    var out = [];
    i = s.length;
    while (i > 0) {
        var c = best_match(i)[0];
        var k = best_match(i)[1];
        if (c == cost[i])
            console.log("Alert: " + c);

        var newToken = true;
        if (s.slice(i - k, i) != "'") {
            if (out.length > 0) {
                if (out[-1] == "'s" || (Number.isInteger(s[i - 1]) && Number.isInteger(out[-1][0]))) {
                    out[-1] = s.slice(i - k, i) + out[-1];
                    newToken = false;
                }
            }
        }

        if (newToken) {
            out.push(s.slice(i - k, i))
        }

        i -= k

    }
    return out.reverse();
}

2

Se precompili l'elenco di parole in un DFA (che sarà molto lento), il tempo necessario per trovare una corrispondenza con un input sarà proporzionale alla lunghezza della stringa (in effetti, solo leggermente più lento rispetto alla semplice iterazione sulla stringa).

Questa è effettivamente una versione più generale dell'algoritmo trie menzionato in precedenza. Lo menziono solo per non completo: per ora non esiste alcuna implementazione di DFA che puoi semplicemente utilizzare. RE2 funzionerebbe, ma non so se i collegamenti Python ti consentano di regolare quanto grande consenti a un DFA di essere prima che getti via i dati DFA compilati ed esegua la ricerca NFA.


soprattutto più per re2, non l'ho usato prima
Sergey

0

Sembra che il backtracking abbastanza banale andrà bene. Inizia dall'inizio della stringa. Scansiona fino a quando non hai una parola. Quindi, chiama la funzione sul resto della stringa. La funzione restituisce "false" se esegue la scansione completamente a destra senza riconoscere una parola. In caso contrario, restituisce la parola trovata e l'elenco di parole restituite dalla chiamata ricorsiva.

Esempio: "tableapple". Trova "tab", quindi "salta", ma nessuna parola in "ple". Nessun'altra parola in "salto". Trova "tavolo", quindi "app". "le" non una parola, così cerca apple, riconosce, ritorna.

Per ottenere il più a lungo possibile, continua, emettendo (anziché restituire) soluzioni corrette; quindi, scegli quello ottimale in base a qualsiasi criterio scelto (maxmax, minmax, media, ecc.)


Buon algoritmo, ci stavo pensando. unutbu ha persino scritto il codice.
Sergey

@Sergey, la ricerca di backtracking è un algoritmo in tempo esponenziale. Cosa c'è di "buono" in questo?
Devin Jeanpierre

1
È semplice, non ho detto che è veloce
Sergey

0

Sulla base della soluzione di unutbu ho implementato una versione Java:

private static List<String> splitWordWithoutSpaces(String instring, String suffix) {
    if(isAWord(instring)) {
        if(suffix.length() > 0) {
            List<String> rest = splitWordWithoutSpaces(suffix, "");
            if(rest.size() > 0) {
                List<String> solutions = new LinkedList<>();
                solutions.add(instring);
                solutions.addAll(rest);
                return solutions;
            }
        } else {
            List<String> solutions = new LinkedList<>();
            solutions.add(instring);
            return solutions;
        }

    }
    if(instring.length() > 1) {
        String newString = instring.substring(0, instring.length()-1);
        suffix = instring.charAt(instring.length()-1) + suffix;
        List<String> rest = splitWordWithoutSpaces(newString, suffix);
        return rest;
    }
    return Collections.EMPTY_LIST;
}

Ingresso: "tableapplechairtablecupboard"

Produzione: [table, apple, chair, table, cupboard]

Ingresso: "tableprechaun"

Produzione: [tab, leprechaun]



0

Espandendo il suggerimento di @ miku di utilizzare a Trie, un'appendice Trieè relativamente semplice da implementare in python:

class Node:
    def __init__(self, is_word=False):
        self.children = {}
        self.is_word = is_word

class TrieDictionary:
    def __init__(self, words=tuple()):
        self.root = Node()
        for word in words:
            self.add(word)

    def add(self, word):
        node = self.root
        for c in word:
            node = node.children.setdefault(c, Node())
        node.is_word = True

    def lookup(self, word, from_node=None):
        node = self.root if from_node is None else from_node
        for c in word:
            try:
                node = node.children[c]
            except KeyError:
                return None

        return node

Possiamo quindi costruire un Triedizionario basato su un insieme di parole:

dictionary = {"a", "pea", "nut", "peanut", "but", "butt", "butte", "butter"}
trie_dictionary = TrieDictionary(words=dictionary)

Che produrrà un albero simile a questo ( *indica l'inizio o la fine di una parola):

* -> a*
 \\\ 
  \\\-> p -> e -> a*
   \\              \-> n -> u -> t*
    \\
     \\-> b -> u -> t*
      \\             \-> t*
       \\                 \-> e*
        \\                     \-> r*
         \
          \-> n -> u -> t*

Possiamo incorporarlo in una soluzione combinandolo con un'euristica su come scegliere le parole. Ad esempio, possiamo preferire parole più lunghe a parole più brevi:

def using_trie_longest_word_heuristic(s):
    node = None
    possible_indexes = []

    # O(1) short-circuit if whole string is a word, doesn't go against longest-word wins
    if s in dictionary:
        return [ s ]

    for i in range(len(s)):
        # traverse the trie, char-wise to determine intermediate words
        node = trie_dictionary.lookup(s[i], from_node=node)

        # no more words start this way
        if node is None:
            # iterate words we have encountered from biggest to smallest
            for possible in possible_indexes[::-1]:
                # recurse to attempt to solve the remaining sub-string
                end_of_phrase = using_trie_longest_word_heuristic(s[possible+1:])

                # if we have a solution, return this word + our solution
                if end_of_phrase:
                    return [ s[:possible+1] ] + end_of_phrase

            # unsolvable
            break

        # if this is a leaf, append the index to the possible words list
        elif node.is_word:
            possible_indexes.append(i)

    # empty string OR unsolvable case 
    return []

Possiamo usare questa funzione in questo modo:

>>> using_trie_longest_word_heuristic("peanutbutter")
[ "peanut", "butter" ]

Perché noi mantenere la nostra posizione nel Triementre cerchiamo più a lungo e parole più lunghe, attraversiamo il trieal massimo una volta per ogni possibile soluzione (anziché 2volte per peanut: pea, peanut). Il cortocircuito finale ci evita di camminare in modo saggio attraverso la corda nel peggiore dei casi.

Il risultato finale è solo una manciata di controlli:

'peanutbutter' - not a word, go charwise
'p' - in trie, use this node
'e' - in trie, use this node
'a' - in trie and edge, store potential word and use this node
'n' - in trie, use this node
'u' - in trie, use this node
't' - in trie and edge, store potential word and use this node
'b' - not in trie from `peanut` vector
'butter' - remainder of longest is a word

Un vantaggio di questa soluzione sta nel fatto che sai molto rapidamente se esistono parole più lunghe con un dato prefisso, il che evita la necessità di testare in modo esaustivo le combinazioni di sequenze rispetto a un dizionario. Rende anche il raggiungimento di una unsolvablerisposta relativamente economico rispetto ad altre implementazioni.

Gli svantaggi di questa soluzione sono un grande ingombro di memoria per il triee il costo della costruzione trieiniziale.


0

Se hai un elenco esaustivo delle parole contenute nella stringa:

word_list = ["table", "apple", "chair", "cupboard"]

Utilizzo della comprensione dell'elenco per scorrere l'elenco per individuare la parola e quante volte appare.

string = "tableapplechairtablecupboard"

def split_string(string, word_list):

    return ("".join([(item + " ")*string.count(item.lower()) for item in word_list if item.lower() in string])).strip()

La funzione restituisce un stringoutput di parole nell'ordine dell'elencotable table apple chair cupboard


0

Molte grazie per l'aiuto in https://github.com/keredson/wordninja/

Un piccolo contributo dello stesso in Java da parte mia.

Il metodo public splitContiguousWordspotrebbe essere incorporato con gli altri 2 metodi della classe con ninja_words.txt nella stessa directory (o modificato secondo la scelta del coder). E il metodo splitContiguousWordspotrebbe essere utilizzato allo scopo.

public List<String> splitContiguousWords(String sentence) {

    String splitRegex = "[^a-zA-Z0-9']+";
    Map<String, Number> wordCost = new HashMap<>();
    List<String> dictionaryWords = IOUtils.linesFromFile("ninja_words.txt", StandardCharsets.UTF_8.name());
    double naturalLogDictionaryWordsCount = Math.log(dictionaryWords.size());
    long wordIdx = 0;
    for (String word : dictionaryWords) {
        wordCost.put(word, Math.log(++wordIdx * naturalLogDictionaryWordsCount));
    }
    int maxWordLength = Collections.max(dictionaryWords, Comparator.comparing(String::length)).length();
    List<String> splitWords = new ArrayList<>();
    for (String partSentence : sentence.split(splitRegex)) {
        splitWords.add(split(partSentence, wordCost, maxWordLength));
    }
    log.info("Split word for the sentence: {}", splitWords);
    return splitWords;
}

private String split(String partSentence, Map<String, Number> wordCost, int maxWordLength) {
    List<Pair<Number, Number>> cost = new ArrayList<>();
    cost.add(new Pair<>(Integer.valueOf(0), Integer.valueOf(0)));
    for (int index = 1; index < partSentence.length() + 1; index++) {
        cost.add(bestMatch(partSentence, cost, index, wordCost, maxWordLength));
    }
    int idx = partSentence.length();
    List<String> output = new ArrayList<>();
    while (idx > 0) {
        Pair<Number, Number> candidate = bestMatch(partSentence, cost, idx, wordCost, maxWordLength);
        Number candidateCost = candidate.getKey();
        Number candidateIndexValue = candidate.getValue();
        if (candidateCost.doubleValue() != cost.get(idx).getKey().doubleValue()) {
            throw new RuntimeException("Candidate cost unmatched; This should not be the case!");
        }
        boolean newToken = true;
        String token = partSentence.substring(idx - candidateIndexValue.intValue(), idx);
        if (token != "\'" && output.size() > 0) {
            String lastWord = output.get(output.size() - 1);
            if (lastWord.equalsIgnoreCase("\'s") ||
                    (Character.isDigit(partSentence.charAt(idx - 1)) && Character.isDigit(lastWord.charAt(0)))) {
                output.set(output.size() - 1, token + lastWord);
                newToken = false;
            }
        }
        if (newToken) {
            output.add(token);
        }
        idx -= candidateIndexValue.intValue();
    }
    return String.join(" ", Lists.reverse(output));
}


private Pair<Number, Number> bestMatch(String partSentence, List<Pair<Number, Number>> cost, int index,
                      Map<String, Number> wordCost, int maxWordLength) {
    List<Pair<Number, Number>> candidates = Lists.reverse(cost.subList(Math.max(0, index - maxWordLength), index));
    int enumerateIdx = 0;
    Pair<Number, Number> minPair = new Pair<>(Integer.MAX_VALUE, Integer.valueOf(enumerateIdx));
    for (Pair<Number, Number> pair : candidates) {
        ++enumerateIdx;
        String subsequence = partSentence.substring(index - enumerateIdx, index).toLowerCase();
        Number minCost = Integer.MAX_VALUE;
        if (wordCost.containsKey(subsequence)) {
            minCost = pair.getKey().doubleValue() + wordCost.get(subsequence).doubleValue();
        }
        if (minCost.doubleValue() < minPair.getKey().doubleValue()) {
            minPair = new Pair<>(minCost.doubleValue(), enumerateIdx);
        }
    }
    return minPair;
}

cosa succede se non abbiamo un elenco di parole?
shirazy

Se ho capito correttamente la query: quindi nell'approccio precedente, il publicmetodo accetta una frase di tipo Stringche viene divisa in base a un primo livello con regex. E per l'elenco ninja_wordsè disponibile per il download dal repository git.
Arnab Das

0

Questo aiuterà

from wordsegment import load, segment
load()
segment('providesfortheresponsibilitiesofperson')


-1

Devi identificare il tuo vocabolario, forse qualsiasi elenco di parole libero andrà bene.

Una volta fatto, usa quel vocabolario per costruire un albero dei suffissi e abbina il tuo flusso di input con quello: http://en.wikipedia.org/wiki/Suffix_tree


Come funzionerebbe in pratica? Dopo aver costruito l'albero dei suffissi, come sapresti cosa abbinare?
John Kurlak

@JohnKurlak Come ogni altro automa finito deterministico, la fine di una parola completa è uno stato di accettazione.
Marcin

Questo approccio non richiede il backtracking? Non hai menzionato il backtracking nella tua risposta ...
John Kurlak

Perchè no? Cosa succede se hai "tableprechaun", come indicato di seguito? Corrisponderà alla parola più lunga possibile, "table", e quindi non troverà un'altra parola. Dovrà tornare indietro alla "scheda" e quindi abbinare "leprechaun".
John Kurlak

@JohnKurlak Più "rami" possono essere attivi contemporaneamente. In effetti, inserisci un gettone nell'albero per ogni lettera che è un possibile inizio di parola, e la stessa lettera può far avanzare altri gettoni attivi.
Marcin
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.