Wie kann man Text ohne Leerzeichen in eine Wortliste aufteilen?


106

Eingabe: "tableapplechairtablecupboard..." viele Wörter

Was wäre ein effizienter Algorithmus, um solchen Text in die Liste der Wörter aufzuteilen und zu erhalten:

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

Das erste, was mir in den Sinn kommt, ist, alle möglichen Wörter (beginnend mit dem ersten Buchstaben) durchzugehen und das längste mögliche Wort zu finden position=word_position+len(word)

PS
Wir haben eine Liste aller möglichen Wörter.
Das Wort "Schrank" kann "Tasse" und "Brett" sein, wählen Sie am längsten.
Sprache: Python, aber Hauptsache ist der Algorithmus selbst.


14
Sind Sie sicher, dass die Zeichenfolge nicht mit den Wörtern "tab" und "leap" beginnt?
Rob Hruska

Ja, es scheint, dass dies nicht eindeutig möglich ist.
Demalexx

@RobHruska, in diesem Fall habe ich geschrieben und die längstmögliche Auswahl getroffen.
Sergey

2
@Sergey - Ihr "längstmögliches" Kriterium implizierte, dass es sich um zusammengesetzte Wörter handelte. Und in diesem Fall, was würde passieren, wenn die Schnur "Zimmermann" wäre. Wäre es "Teppich" oder "Sturmvogel"?
Rob Hruska

2
Es gibt viele diktatorische Wörter in Ihrer Zeichenfolge:['able', 'air', 'apple', 'boa', 'boar', 'board', 'chair', 'cup', 'cupboard', 'ha', 'hair', 'lea', 'leap', 'oar', 'tab', 'table', 'up']
reclosedev

Antworten:


199

Ein naiver Algorithmus liefert keine guten Ergebnisse, wenn er auf reale Daten angewendet wird. Hier ist ein 20-Zeilen-Algorithmus, der die relative Worthäufigkeit ausnutzt, um genaue Ergebnisse für Echtworttext zu erhalten.

(Wenn Sie eine Antwort auf Ihre ursprüngliche Frage wünschen, bei der die Worthäufigkeit nicht verwendet wird, müssen Sie verfeinern, was genau unter "längstes Wort" zu verstehen ist: Ist es besser, ein Wort mit 20 Buchstaben und zehn Wörter mit drei Buchstaben zu haben, oder ist dies der Fall? Es ist besser, fünf Wörter mit 10 Buchstaben zu haben. Sobald Sie sich für eine genaue Definition entschieden haben, müssen Sie nur noch die Liniendefinition ändern wordcost, um die beabsichtigte Bedeutung wiederzugeben.)

Die Idee

Der beste Weg, um fortzufahren, besteht darin , die Verteilung der Ausgabe zu modellieren . Eine gute erste Annäherung ist die Annahme, dass alle Wörter unabhängig voneinander verteilt sind. Dann müssen Sie nur noch die relative Häufigkeit aller Wörter kennen. Es ist anzunehmen, dass sie dem Zipf-Gesetz folgen, dh das Wort mit Rang n in der Liste der Wörter hat eine Wahrscheinlichkeit von ungefähr 1 / ( n log N ), wobei N die Anzahl der Wörter im Wörterbuch ist.

Sobald Sie das Modell repariert haben, können Sie mithilfe der dynamischen Programmierung auf die Position der Räume schließen. Der wahrscheinlichste Satz ist derjenige, der das Produkt der Wahrscheinlichkeit jedes einzelnen Wortes maximiert, und es ist einfach, ihn mit dynamischer Programmierung zu berechnen. Anstatt die Wahrscheinlichkeit direkt zu verwenden, verwenden wir Kosten, die als Logarithmus der Umkehrung der Wahrscheinlichkeit definiert sind, um Überläufe zu vermeiden.

Der Code

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))

mit denen Sie verwenden können

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

Die Ergebnisse

Ich verwende dieses schnelle und schmutzige Wörterbuch mit 125.000 Wörtern, das ich aus einer kleinen Teilmenge von Wikipedia zusammengestellt habe.

Vorher: thumbgreenappleactiveassignmentweeklymetaphor.
Nachher: Daumengrün Apfel aktive Zuordnung wöchentliche Metapher.

Vorher: Es gibt eine Reihe von Textinformationen von Personenkommentaren, die aus dem ml herausgesucht wurden, aber keine begrenzten Zeichen in der vorherigen Beispiel-Grün-App-Aktivierungszuweisung enthalten sind.

Nachher: ​​Es gibt eine Menge Textinformationen von Kommentaren von Leuten, die aus HTML analysiert werden, aber es gibt keine abgegrenzten Zeichen in ihnen, zum Beispiel Daumengrünapfel, aktive Zuordnung, wöchentliche Metapher, anscheinend gibt es Daumengrünapfel usw. in der Zeichenfolge, zu der ich auch ein großes Wörterbuch habe Fragen Sie, ob das Wort vernünftig ist. Was ist der schnellste Weg, um viel zu extrahieren?

Vorher: Es war dunkel und stürmisch, und es kam zu heftigen Abständen, außer bei gelegentlichen Intervallen, bei denen die Straßen wegen einer Entzündung in der Mitte der Straßen, die zwischen den Hausoberseiten klapperten, heftig klapperten und die Flamme der Lampe, die stark bekämpft war, heftig klapperten.

Nachher: Es war eine dunkle und stürmische Nacht, in der der Regen in Strömen fiel, außer in gelegentlichen Abständen, als er von einem heftigen Windstoß kontrolliert wurde, der die Straßen fegte, denn in London liegt unsere Szene rasselnd auf den Hausdächern und bewegt die spärliche Flamme der Lampen, die gegen die Dunkelheit kämpften.

Wie Sie sehen können, ist es im Wesentlichen einwandfrei. Der wichtigste Teil ist, sicherzustellen, dass Ihre Wortliste auf einen Korpus trainiert wurde, der dem ähnelt, auf den Sie tatsächlich stoßen, da sonst die Ergebnisse sehr schlecht sind.


Optimierung

Die Implementierung verbraucht linear viel Zeit und Speicher, ist also einigermaßen effizient. Wenn Sie weitere Beschleunigungen benötigen, können Sie aus der Wortliste einen Suffixbaum erstellen, um die Größe der Kandidatenmenge zu verringern.

Wenn Sie eine sehr große aufeinanderfolgende Zeichenfolge verarbeiten müssen, ist es sinnvoll, die Zeichenfolge zu teilen, um eine übermäßige Speichernutzung zu vermeiden. Sie können den Text beispielsweise in Blöcken mit 10000 Zeichen plus einem Rand von 1000 Zeichen auf beiden Seiten verarbeiten, um Randeffekte zu vermeiden. Dies reduziert die Speichernutzung auf ein Minimum und hat mit ziemlicher Sicherheit keinen Einfluss auf die Qualität.


1
Was ist mit zweizeiligem Text?
Blatt

11
Dieser Code hat mich taub gemacht. Ich habe kein bisschen verstanden. Ich verstehe keine Protokollsachen. Aber ich habe diesen Code auf meinem Computer getestet. Du bist ein Genie.
Aditya Singh

1
Was ist die Laufzeit dieses Algorithmus? Warum benutzt du keinen Ahocorasick?
RetroCode

8
Das ist ausgezeichnet. Ich habe es in ein Pip-Paket verwandelt : pypi.python.org/pypi/wordninjapip install wordninja
keredson

2
@wittrup Ihr words.txtenthält "comp": `` `$ grep" ^ comp $ "words.txt comp` `` und es ist alphabetisch sortiert. Dieser Code geht davon aus, dass er in abnehmender Häufigkeit des Auftretens sortiert ist (was bei solchen n-Gramm-Listen üblich ist). Wenn Sie eine ordnungsgemäß sortierte Liste verwenden, wird Ihre Zeichenfolge gut angezeigt: `` >>> wordninja.split ('namethecompanywherebonniewasemployedwhenwestarteddating') ['name', 'the', 'company', 'where', 'bonnie', ' war ',' angestellt ',' wann ',' wir ',' angefangen ',' datiert '] `` `
keredson

49

Basierend auf der hervorragenden Arbeit in der Top-Antwort habe ich ein pipPaket für die einfache Verwendung erstellt.

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

Führen Sie zum Installieren aus pip install wordninja.

Die einzigen Unterschiede sind gering. Dies gibt listeher ein als ein zurück str, es funktioniert in python3, es enthält die Wortliste und wird ordnungsgemäß aufgeteilt, selbst wenn Nicht-Alpha-Zeichen (wie Unterstriche, Bindestriche usw.) vorhanden sind.

Nochmals vielen Dank an Generic Human!

https://github.com/keredson/wordninja


2
Vielen Dank für die Erstellung.
Mohit Bhatia

1
Danke dir! Ich liebe es, dass du daraus ein Paket gemacht hast. Die zugrunde liegende Methode hat bei mir nicht sehr gut funktioniert. Zum Beispiel wurden die "Liegen" in "Lounge" und "rs" aufgeteilt
Harry M

@keredson - Zunächst einmal vielen Dank für die Lösung. Es verhält sich gut. Es werden jedoch Sonderzeichen wie "-" usw. entfernt. Manchmal wird nicht die richtige Aufteilung vorgenommen, z. B. "WeatheringPropertiesbyMaterial Trade Name Graph 2-1. Farbänderung, E, nach Arizona, Florida, Cycolac® / Geloy®-Harzsysteme im Vergleich zu PVC. [15] 25 20 15 ∆E 10 5 0 PVC, weißes PVC, braunes C / G, braunes C / G. Capstock ist das Material, das als Oberflächenschicht für die Außenfläche eines Profils verwendet wird Extrusion. Geloy®-Harzdeckel über einem Cycolac®-Substrat bietet hervorragende Witterungsbeständigkeit. [25] "
Rakesh Lamp Stack

Können Sie ein Problem in GH eröffnen?
Keredson

1
Gute Arbeit, danke für die Mühe. Das hat mir wirklich viel Zeit gespart.
Jan Zeiseweis

17

Hier ist eine Lösung mit rekursiver Suche:

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'])))

ergibt

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

funktioniert "out of the box", danke! Ich denke auch, die Struktur zu verwenden, wie Miku sagte, nicht nur alle Wörter. Danke trotzdem!
Sergey

11

Bei Verwendung einer Trie- Datenstruktur , die die Liste möglicher Wörter enthält, wäre es nicht zu kompliziert, Folgendes zu tun:

  1. Vorwärtszeiger (in der verketteten Zeichenfolge)
  2. Suchen und speichern Sie den entsprechenden Knoten im Trie
  3. Wenn der Trie-Knoten untergeordnete Knoten hat (z. B. gibt es längere Wörter), fahren Sie mit 1 fort.
  4. Wenn der erreichte Knoten keine untergeordneten Knoten hat, ist eine längste Wortübereinstimmung aufgetreten. Fügen Sie das Wort (im Knoten gespeichert oder nur während des Durchlaufs verkettet) zur Ergebnisliste hinzu, setzen Sie den Zeiger im Versuch zurück (oder setzen Sie die Referenz zurück) und beginnen Sie von vorne

3
Wenn das Ziel darin besteht, die gesamte Zeichenfolge zu verbrauchen, müssten Sie zurückverfolgen und müssen "tableprechaun"danach aufgeteilt werden "tab".
Daniel Fischer

Plus für die Erwähnung von Trie, aber ich stimme auch Daniel zu, dass Backtracking gemacht werden muss.
Sergey

@ Daniel, die Suche mit der längsten Übereinstimmung erfordert kein Backtracking, nein. Was bringt dich dazu, das zu denken? Und was ist mit dem obigen Algorithmus falsch?
Devin Jeanpierre

1
@Devin Die Tatsache, dass für "tableprechaun"die längste Übereinstimmung von Anfang an ist "table", verlassen "prechaun", die nicht in Wörterbuchwörter aufgeteilt werden kann. Sie müssen also das kürzere Match nehmen "tab"und haben ein "leprechaun".
Daniel Fischer

@ Daniel, Entschuldigung, ja. Ich habe das Problem falsch verstanden. Der korrigierte Algorithmus sollte alle möglichen Baumpositionen gleichzeitig verfolgen - AKA-NFA-Suche mit linearer Zeit. Oder zurück, klar, aber das ist die exponentielle Zeit im schlimmsten Fall.
Devin Jeanpierre

9

Die Lösung von Unutbu war ziemlich nah, aber ich finde den Code schwer zu lesen und er lieferte nicht das erwartete Ergebnis. Die Lösung von Generic Human hat den Nachteil, dass Wortfrequenzen benötigt werden. Nicht für alle Anwendungsfälle geeignet.

Hier ist eine einfache Lösung mit einem Divide and Conquer-Algorithmus .

  1. Es versucht, die Anzahl der Wörter zu minimieren Eg find_words('cupboard')zurückkehren wird , ['cupboard']anstatt ['cup', 'board'](dh unter der Annahme cupboard, cupund boardsind in der Dictionnary)
  2. Die optimale Lösung ist nicht eindeutig . Die folgende Implementierung gibt eine Lösung zurück. find_words('charactersin')könnte zurückkehren ['characters', 'in']oder vielleicht wird es zurückkehren ['character', 'sin'](wie unten gezeigt). Sie können den Algorithmus ganz einfach ändern, um alle optimalen Lösungen zurückzugeben.
  3. In dieser Implementierung werden Lösungen so gespeichert , dass sie in einer angemessenen Zeit ausgeführt werden.

Der Code:

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

Dies dauert auf meinem 3-GHz-Computer ungefähr 5 Sekunden:

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

Die reis Massen von Textinformationen von Kommentaren der Menschen, die von HTML analysiert werden, aber es gibt kein abgegrenztes Zeichen sin sie zum Beispiel Daumen grüner Apfel aktive Zuordnung wöchentliche Metapher anscheinend gibt es Daumen grüner Apfel usw. in der Zeichenfolge Ich habe auch ein großes Wörterbuch, um zu fragen, ob Das Wort ist vernünftig, also was ist der schnellste Weg, um viel zu extrahieren


Es gibt keinen Grund zu der Annahme, dass ein Text nicht mit einem Wort aus einem Buchstaben enden kann. Sie sollten einen Split mehr in Betracht ziehen.
Panda-34

7

Die Antwort von https://stackoverflow.com/users/1515832/generic-human ist großartig. Aber die beste Umsetzung, die ich je gesehen habe, war Peter Norvig selbst in seinem Buch 'Beautiful Data'.

Bevor ich seinen Code einfüge, möchte ich erläutern, warum Norvigs Methode genauer ist (obwohl sie in Bezug auf den Code etwas langsamer und länger ist).

1) Die Daten sind etwas besser - sowohl in Bezug auf die Größe als auch in Bezug auf die Genauigkeit (er verwendet eine Wortzahl anstelle einer einfachen Rangfolge). 2) Noch wichtiger ist, dass die Logik hinter n-Gramm den Ansatz wirklich so genau macht .

Das Beispiel, das er in seinem Buch liefert, ist das Problem des Aufteilens einer Zeichenfolge "Sitdown". Jetzt würde eine Nicht-Bigram-Methode der Zeichenfolgenaufteilung p ('sit') * p ('down') berücksichtigen, und wenn dies weniger als p ('sitdown') ist - was ziemlich oft der Fall sein wird - wird es NICHT aufgeteilt es, aber wir würden es wollen (meistens).

Wenn Sie jedoch das Bigram-Modell haben, können Sie p ("hinsetzen") als Bigram gegen p ("sitzen") bewerten, und das erstere gewinnt. Wenn Sie keine Bigramme verwenden, wird die Wahrscheinlichkeit, dass die Wörter, die Sie teilen, als unabhängig behandelt, grundsätzlich behandelt. Dies ist jedoch nicht der Fall. Einige Wörter werden eher nacheinander angezeigt. Leider sind dies auch die Wörter, die in vielen Fällen oft zusammenkleben und den Splitter verwirren.

Hier ist der Link zu den Daten (es sind Daten für 3 verschiedene Probleme und die Segmentierung ist nur eine. Bitte lesen Sie das Kapitel für Details): http://norvig.com/ngrams/

und hier ist der Link zum Code: http://norvig.com/ngrams/ngrams.py

Diese Links sind schon eine Weile aktiv, aber ich werde den Segmentierungsteil des Codes hier trotzdem kopieren und einfügen

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 

Das funktioniert gut, aber wenn ich versuche, dies auf meinen gesamten Datensatz anzuwenden, heißt es immer wiederRuntimeError: maximum recursion depth exceeded in cmp
Harry M

ngrams geben Ihnen definitiv einen Genauigkeitsschub mit einem exponentiell größeren Frequenzdiktat, Speicher- und Rechenaufwand. Übrigens verliert die Memofunktion dort Speicher wie ein Sieb. sollte es zwischen Anrufen löschen.
Keredson

3

Hier ist die akzeptierte Antwort, die in JavaScript übersetzt wurde (erfordert node.js und die Datei "wordninja_words.txt" von 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

Wenn Sie die Wortliste in einen DFA vorkompilieren (was sehr langsam sein wird), ist die Zeit, die zum Abgleichen einer Eingabe benötigt wird, proportional zur Länge der Zeichenfolge (tatsächlich nur ein wenig langsamer als nur das Durchlaufen der Zeichenfolge).

Dies ist effektiv eine allgemeinere Version des zuvor erwähnten Trie-Algorithmus. Ich erwähne es nur für vollständig - bis jetzt gibt es keine DFA-Implementierung, die Sie nur verwenden können. RE2 würde funktionieren, aber ich weiß nicht, ob Sie mit den Python-Bindungen einstellen können, wie groß ein DFA sein darf, bevor er nur die kompilierten DFA-Daten wegwirft und die NFA-Suche durchführt.


besonders plus für re2, habe es vorher nicht benutzt
Sergey

0

Es scheint, als würde ein ziemlich banales Backtracking ausreichen. Beginnen Sie am Anfang der Saite. Scannen Sie nach rechts, bis Sie ein Wort haben. Rufen Sie dann die Funktion für den Rest der Zeichenfolge auf. Die Funktion gibt "false" zurück, wenn sie ganz nach rechts scannt, ohne ein Wort zu erkennen. Andernfalls wird das gefundene Wort und die Liste der vom rekursiven Aufruf zurückgegebenen Wörter zurückgegeben.

Beispiel: "Tableapple". Findet "tab", dann "leap", aber kein Wort in "ple". Kein anderes Wort in "leapple". Findet "Tabelle" und dann "App". "le" kein Wort, also versucht Apfel, erkennt, kehrt zurück.

Um so lange wie möglich zu werden, machen Sie weiter und geben Sie nur korrekte Lösungen heraus (anstatt sie zurückzugeben). Wählen Sie dann das optimale anhand eines von Ihnen gewählten Kriteriums (Maxmax, Minmax, Durchschnitt usw.).


Guter Algorithmus, dachte darüber nach. unutbu hat sogar den Code geschrieben.
Sergey

@Sergey, die Backtracking-Suche ist ein Exponential-Time-Algorithmus. Was ist daran "gut"?
Devin Jeanpierre

1
Es ist einfach, hat nicht gesagt, dass es schnell ist
Sergey

0

Basierend auf der Lösung von unutbu habe ich eine Java-Version implementiert:

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;
}

Eingang: "tableapplechairtablecupboard"

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

Eingang: "tableprechaun"

Ausgabe: [tab, leprechaun]



0

Wenn Sie den Vorschlag von @ miku zur Verwendung von a erweitern Trie, Trieist die Implementierung eines Nur-Anhängens relativ einfach 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

Wir können dann ein TrieWörterbuch auf Basis einer Reihe von Wörtern erstellen :

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

Was einen Baum erzeugt, der so aussieht ( *zeigt den Anfang oder das Ende eines Wortes an):

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

Wir können dies in eine Lösung integrieren, indem wir es mit einer Heuristik über die Auswahl von Wörtern kombinieren. Zum Beispiel können wir längere Wörter kürzeren Wörtern vorziehen:

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 []

Wir können diese Funktion folgendermaßen verwenden:

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

Weil wir unsere Position in der halten Triewir für mehr und längere Wörter suchen, durchqueren wir die triehöchstens einmal pro mögliche Lösung ( und nicht 2mal für peanut: pea, peanut). Der letzte Kurzschluss erspart uns im schlimmsten Fall, char-weise durch die Saite zu gehen.

Das Endergebnis ist nur eine Handvoll Inspektionen:

'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

Ein Vorteil dieser Lösung besteht darin, dass Sie sehr schnell wissen, ob längere Wörter mit einem bestimmten Präfix vorhanden sind, sodass Sie die Sequenzkombinationen nicht ausführlich anhand eines Wörterbuchs testen müssen. Es macht es auch unsolvablevergleichsweise billig, eine Antwort zu einer anderen Implementierung zu finden.

Die Nachteile dieser Lösung sind ein großer Speicherbedarf für die trieund die Kosten für den trieAufbau des Vorlaufs.


0

Wenn Sie eine vollständige Liste der in der Zeichenfolge enthaltenen Wörter haben:

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

Verwenden des Listenverständnisses zum Durchlaufen der Liste, um das Wort zu lokalisieren und wie oft es erscheint.

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()

Die Funktion gibt eine stringAusgabe von Wörtern in der Reihenfolge der Liste zurücktable table apple chair cupboard


0

Vielen Dank für die Hilfe in https://github.com/keredson/wordninja/

Ein kleiner Beitrag davon in Java von meiner Seite.

Die öffentliche Methode splitContiguousWordskann in die anderen beiden Methoden der Klasse eingebettet werden, die ninja_words.txt im selben Verzeichnis haben (oder gemäß der Wahl des Codierers geändert werden). Und die Methode splitContiguousWordskönnte für diesen Zweck verwendet werden.

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;
}

Was ist, wenn wir keine Wortliste haben?
Shirazy

Wenn ich die Abfrage richtig verstanden habe: Daher publicakzeptiert die Methode im obigen Ansatz einen Satz vom Typ, Stringder basierend auf einer ersten Ebene mit Regex aufgeteilt wird. Und für die Liste ninja_wordssteht es zum Download im Git Repo zur Verfügung.
Arnab Das


-1

Sie müssen Ihren Wortschatz identifizieren - vielleicht reicht jede freie Wortliste.

Verwenden Sie anschließend dieses Vokabular, um einen Suffixbaum zu erstellen, und vergleichen Sie Ihren Eingabestream mit dem folgenden: http://en.wikipedia.org/wiki/Suffix_tree


Wie würde das in der Praxis funktionieren? Woher wissen Sie nach dem Erstellen des Suffixbaums, was zu Ihnen passt?
John Kurlak

@ JohnKurlak Wie jeder andere deterministische endliche Automat ist das Ende eines vollständigen Wortes ein akzeptierender Zustand.
Marcin

Erfordert dieser Ansatz nicht ein Backtracking? Sie haben Backtracking in Ihrer Antwort nicht erwähnt ...
John Kurlak

Warum nicht? Was passiert, wenn Sie "Tischkobold" haben, wie unten erwähnt? Es wird mit dem längsten Wort übereinstimmen, das es kann, "Tabelle", und dann wird es kein anderes Wort finden. Es muss zurück zu "tab" gehen und dann "leprechaun" entsprechen.
John Kurlak

@ JohnKurlak Mehrere "Zweige" können gleichzeitig live sein. Tatsächlich schieben Sie für jeden Buchstaben, der ein möglicher Wortstart ist, einen Token in den Baum, und derselbe Buchstabe kann andere Live-Token voranbringen.
Marcin
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.