Comment diviser du texte sans espaces en liste de mots?


106

Entrée: "tableapplechairtablecupboard..." beaucoup de mots

Quel serait un algorithme efficace pour diviser ce texte en une liste de mots et obtenir:

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

La première chose qui me vient à l'esprit est de parcourir tous les mots possibles (en commençant par la première lettre) et de trouver le mot le plus long possible, continuer à partir de position=word_position+len(word)

PS
Nous avons une liste de tous les mots possibles.
Le mot «placard» peut être «tasse» et «planche», sélectionnez le plus long.
Langage: python, mais l'essentiel est l'algorithme lui-même.


14
Êtes-vous sûr que cette chaîne ne commence pas par les mots «tab» et «saut»?
Rob Hruska

Oui, il semble que cela ne puisse pas être fait de manière non ambiguë.
demalexx

@RobHruska, dans ce cas, j'ai écrit, en sélectionnant le plus longtemps possible.
Sergey

2
@Sergey - Votre critère "le plus long possible" impliquait qu'il s'agissait de mots composés. Et dans ce cas, que se passerait-il si la corde était "tapisrel". Serait-ce «tapis» ou «pétrel»?
Rob Hruska

2
Votre chaîne contient de nombreux mots dictionnaires:['able', 'air', 'apple', 'boa', 'boar', 'board', 'chair', 'cup', 'cupboard', 'ha', 'hair', 'lea', 'leap', 'oar', 'tab', 'table', 'up']
reclosedev

Réponses:


200

Un algorithme naïf ne donnera pas de bons résultats lorsqu'il est appliqué à des données du monde réel. Voici un algorithme de 20 lignes qui exploite la fréquence relative des mots pour donner des résultats précis pour le texte de mots réels.

(Si vous voulez une réponse à votre question initiale qui n'utilise pas la fréquence des mots, vous devez préciser ce que l'on entend exactement par "mot le plus long": vaut-il mieux avoir un mot de 20 lettres et dix mots de 3 lettres, ou est il vaut mieux avoir cinq mots de 10 lettres? Une fois que vous avez choisi une définition précise, il vous suffit de changer la ligne définissant wordcostpour refléter le sens voulu.)

L'idée

La meilleure façon de procéder est de modéliser la distribution de la sortie. Une bonne première approximation consiste à supposer que tous les mots sont distribués indépendamment. Ensuite, il vous suffit de connaître la fréquence relative de tous les mots. Il est raisonnable de supposer qu'ils suivent la loi de Zipf, c'est-à-dire que le mot de rang n dans la liste des mots a une probabilité d'environ 1 / ( n log N ) où N est le nombre de mots du dictionnaire.

Une fois que vous avez fixé le modèle, vous pouvez utiliser la programmation dynamique pour déduire la position des espaces. La phrase la plus probable est celle qui maximise le produit de la probabilité de chaque mot individuel, et il est facile de la calculer avec une programmation dynamique. Au lieu d'utiliser directement la probabilité, nous utilisons un coût défini comme le logarithme de l'inverse de la probabilité pour éviter les débordements.

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

que vous pouvez utiliser avec

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

Les resultats

J'utilise ce dictionnaire rapide et sale de 125 000 mots que j'ai créé à partir d'un petit sous-ensemble de Wikipédia.

Avant: thumbgreenappleactiveassignmentweeklymetaphor.
Après: pouce vert pomme affectation active métaphore hebdomadaire.

Avant: informations textuellespourlespersonnescommentairesqui sont analysésà partir dehtmlmaisd'entendreunpersonnages délimitésdansempourexampletourdetappliqueractiverl'affectation chaque semaine

Après: il y a des masses d'informations textuelles sur les commentaires des peuples qui sont analysées à partir de html mais il n'y a pas de caractères délimités, par exemple pouce vert pomme affectation active métaphore hebdomadaire apparemment il y a pouce pomme verte etc. dans la chaîne j'ai aussi un grand dictionnaire à demander si le mot est raisonnable, donc quel est le moyen le plus rapide d'extraction, merci beaucoup.

Avant: sombre et tempêteune nuitd'autre est tombéenorrentexceptàdesintervalles occasionnelsquand elle a été vérifiéepar une rafale de vent violente qui a balayé les stèles profondspour lesitesenlondontquotourcenelies se disputant le long des dizaines de kilomètres etfouragitecela

Après: c'était une nuit sombre et orageuse la pluie est tombée à torrents sauf à des intervalles occasionnels lorsqu'elle a été arrêtée par une violente rafale de vent qui a balayé les rues car c'est à Londres que notre scène tremble le long des toits et agite férocement le faible flamme des lampes qui luttaient contre les ténèbres.

Comme vous pouvez le voir, il est essentiellement impeccable. La partie la plus importante est de vous assurer que votre liste de mots a été formée à un corpus similaire à ce que vous rencontrerez réellement, sinon les résultats seront très mauvais.


Optimisation

L'implémentation consomme une quantité linéaire de temps et de mémoire, elle est donc raisonnablement efficace. Si vous avez besoin d'accélérations supplémentaires, vous pouvez créer une arborescence de suffixes à partir de la liste de mots pour réduire la taille de l'ensemble de candidats.

Si vous devez traiter une très grande chaîne consécutive, il serait raisonnable de diviser la chaîne pour éviter une utilisation excessive de la mémoire. Par exemple, vous pouvez traiter le texte en blocs de 10 000 caractères plus une marge de 1 000 caractères de chaque côté pour éviter les effets de limite. Cela maintiendra l'utilisation de la mémoire au minimum et n'aura presque certainement aucun effet sur la qualité.


1
qu'en est-il du texte sur deux lignes?
leafiy

11
Ce code m'a rendu insensible. Je n'ai pas compris un peu. Je ne comprends pas les choses du journal. Mais j'ai testé ce code sur mon ordinateur. Tu es un génie.
Aditya Singh

1
Quelle est la durée d'exécution de cet algorithme? Pourquoi n'utilisez-vous pas ahocorasick?
RetroCode du

8
C'est excellent. Je l'ai transformé en un package pip: pypi.python.org/pypi/wordninja pip install wordninja
keredson

2
@wittrup votre words.txtcontient "comp": `` `` $ grep "^ comp $" words.txt comp `` `et il est trié par ordre alphabétique. ce code suppose qu'il est trié selon une fréquence d'apparition décroissante (ce qui est courant pour les listes de n-grammes comme celle-ci). si vous utilisez une liste correctement triée, votre chaîne sort bien: `` `` >>> wordninja.split ('namethecompanywherebonniewasemployed whenwestarteddating') ['name', 'the', 'company', 'where', 'bonnie', ' était ',' employé ',' quand ',' nous ',' avons 'commencé', 'sortir ensemble'] `` ``
keredson

50

Sur la base de l'excellent travail de la première réponse , j'ai créé un pippackage pour une utilisation facile.

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

Pour installer, exécutez pip install wordninja .

Les seules différences sont mineures. Cela renvoie a listplutôt que a str, cela fonctionne enpython3 , il inclut la liste de mots et se divise correctement même s'il y a des caractères non alpha (comme des traits de soulignement, des tirets, etc.).

Merci encore à Generic Human!

https://github.com/keredson/wordninja


2
Merci d'avoir créé ça.
Mohit Bhatia

1
Je vous remercie! J'adore que vous en ayez fait un paquet. La méthode sous-jacente n'a pas très bien fonctionné pour moi. Par exemple, les "chaises longues" ont été divisées en "lounge" et "rs"
Harry M

@keredson - Tout d'abord, merci pour la solution. Il se comporte bien. Cependant, il supprime les caractères spéciaux tels que "-" etc. Parfois, il ne donne pas la séparation appropriée, comme prendre une longue chaîne, disons - "WeatheringPropertiesbyMaterial Trade Name Graph 2-1. Changement de couleur, E, après Arizona, Florida, Cycolac® / Systèmes de résine Geloy® par rapport au PVC. [15] 25 20 15 ∆E 10 5 0 PVC, PVC blanc, brun C / G, brun C / G. Capstock est le matériau utilisé comme couche de surface appliquée sur la surface extérieure d'un profilé extrusion. La résine de recouvrement Geloy® sur un substrat Cycolac® offre une excellente résistance aux intempéries. [25] "
Rakesh Lamp Stack

pouvez-vous ouvrir un problème dans GH?
keredson

1
Beau travail, merci pour l'effort. Cela m'a vraiment fait gagner beaucoup de temps.
Jan Zeiseweis

17

Voici la solution utilisant la recherche récursive:

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

rendements

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

fonctionne "hors de la boîte", merci! Je pense aussi utiliser la structure du trie comme l'a dit miku, pas seulement un ensemble de tous les mots. Merci quand même!
Sergey

11

En utilisant une structure de données trie , qui contient la liste des mots possibles, il ne serait pas trop compliqué de faire ce qui suit:

  1. Pointeur avancé (dans la chaîne concaténée)
  2. Rechercher et stocker le nœud correspondant dans le trie
  3. Si le nœud trie a des enfants (par exemple, il y a des mots plus longs), passez à 1.
  4. Si le nœud atteint n'a pas d'enfants, une correspondance de mot la plus longue s'est produite; ajouter le mot (stocké dans le nœud ou juste concaténé lors du parcours du trie) à la liste de résultats, réinitialiser le pointeur dans le tri (ou réinitialiser la référence), et recommencer

3
Si la cible doit consommer la chaîne entière, vous devrez revenir en arrière, "tableprechaun"puis diviser par la suite "tab".
Daniel Fischer

De plus pour avoir mentionné le trie, mais je suis également d'accord avec Daniel, qu'il faut revenir en arrière.
Sergey

@Daniel, la recherche de correspondance la plus longue n'a pas besoin de retour en arrière, non. Qu'est ce qui te fait penser ça? Et quel est le problème avec l'algorithme ci-dessus?
Devin Jeanpierre

1
@Devin Le fait que pour "tableprechaun"la plus longue correspondance depuis le début est "table", partir "prechaun", qui ne peut pas être divisé en mots du dictionnaire. Vous devez donc prendre le match le plus court "tab"en vous laissant avec un "leprechaun".
Daniel Fischer

@Daniel, désolé, oui. J'ai mal compris le problème. L'algorithme corrigé doit garder une trace de toutes les positions d'arbre possibles à la fois - recherche NFA en temps linéaire AKA. Ou bien revenir en arrière, bien sûr, mais c'est le pire des cas exponentiels.
Devin Jeanpierre

9

La solution d'Unutbu était assez proche mais je trouve le code difficile à lire et il n'a pas donné le résultat attendu. La solution de Generic Human a l'inconvénient d'avoir besoin de fréquences de mots. Ne convient pas à tous les cas d'utilisation.

Voici une solution simple utilisant un algorithme Divide and Conquer .

  1. Il essaie de minimiser le nombre de mots que Eg find_words('cupboard')retournera ['cupboard']plutôt que ['cup', 'board'](en supposant que cupboard, cupetboard sont dans le dictionnaire)
  2. La solution optimale n'est pas unique , l'implémentation ci-dessous renvoie une solution. find_words('charactersin')pourrait revenir ['characters', 'in']ou peut-être qu'il reviendra ['character', 'sin'](comme vu ci-dessous). Vous pouvez facilement modifier l'algorithme pour renvoyer toutes les solutions optimales.
  3. Dans cette implémentation, les solutions sont mémorisées pour qu'elle s'exécute dans un délai raisonnable.

Le 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

Cela prendra environ 5 secondes sur ma machine 3GHz:

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

les masses reis d'informations textuelles des commentaires des peuples qui sont analysées à partir de html mais il n'y a pas de caractère délimité les péchés par exemple pouce vert pomme affectation active métaphore hebdomadaire apparemment il y a pouce pomme verte etc. dans la chaîne j'ai aussi un grand dictionnaire pour demander si le mot est raisonnable alors quel est le moyen le plus rapide d'extraction thxa beaucoup


Il n'y a aucune raison de croire qu'un texte ne peut pas se terminer par un mot d'une seule lettre. Vous devriez envisager une scission de plus.
panda-34

7

La réponse de https://stackoverflow.com/users/1515832/generic-human est excellente. Mais la meilleure implémentation que j'ai jamais vue a été écrite par Peter Norvig lui-même dans son livre «Beautiful Data».

Avant de coller son code, permettez-moi d'expliquer pourquoi la méthode de Norvig est plus précise (bien qu'un peu plus lente et plus longue en termes de code).

1) Les données sont un peu meilleures - à la fois en termes de taille et de précision (il utilise un nombre de mots plutôt qu'un simple classement) 2) Plus important encore, c'est la logique derrière les n-grammes qui rend vraiment l'approche si précise .

L'exemple qu'il donne dans son livre est le problème de la division d'une chaîne «sitdown». Maintenant, une méthode non bigramme de division de chaîne considérerait p ('sit') * p ('down'), et si cela est inférieur au p ('sitdown') - ce qui sera le cas assez souvent - il ne sera PAS divisé mais nous le voudrions (la plupart du temps).

Cependant, lorsque vous avez le modèle bigramme, vous pouvez évaluer p («s'asseoir») comme un bigramme vs p («sitdown») et le premier gagne. En gros, si vous n'utilisez pas de bigrammes, cela traite la probabilité des mots que vous divisez comme indépendants, ce qui n'est pas le cas, certains mots sont plus susceptibles d'apparaître les uns après les autres. Malheureusement, ce sont aussi les mots qui sont souvent collés ensemble dans de nombreux cas et qui confondent le séparateur.

Voici le lien vers les données (il s'agit de données pour 3 problèmes distincts et la segmentation n'en est qu'un. Veuillez lire le chapitre pour plus de détails): http://norvig.com/ngrams/

et voici le lien vers le code: http://norvig.com/ngrams/ngrams.py

Ces liens existent depuis un certain temps, mais je vais quand même copier-coller la partie segmentation du code ici

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 

Cela fonctionne bien, mais quand j'essaie de l'appliquer sur tout mon ensemble de données, cela ne cesse de direRuntimeError: maximum recursion depth exceeded in cmp
Harry M

ngrams vous donnera certainement une amélioration de la précision avec une fréquence exponentiellement plus élevée, la mémoire et l'utilisation des calculs. btw la fonction mémo fuit la mémoire comme un tamis là-bas. devrait l'effacer entre les appels.
keredson du

3

Voici la réponse acceptée traduite en JavaScript (nécessite node.js et le fichier "wordninja_words.txt" de 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

Si vous précompilez la liste de mots dans un DFA (ce qui sera très lent), le temps nécessaire pour faire correspondre une entrée sera proportionnel à la longueur de la chaîne (en fait, seulement un peu plus lent qu'une simple itération sur la chaîne).

Il s'agit en fait d'une version plus générale de l'algorithme trie qui a été mentionné précédemment. Je ne le mentionne que pour une utilisation complète - pour l'instant, il n'y a pas d'implémentation DFA que vous pouvez simplement utiliser. RE2 fonctionnerait, mais je ne sais pas si les liaisons Python vous permettent de régler la taille que vous autorisez à un DFA avant qu'il ne jette simplement les données DFA compilées et effectue une recherche NFA.


surtout plus pour re2, je ne l'ai pas utilisé auparavant
Sergey

0

Il semble qu'un retour en arrière assez banal fera l'affaire. Commencez au début de la chaîne. Scannez à droite jusqu'à ce que vous ayez un mot. Ensuite, appelez la fonction sur le reste de la chaîne. La fonction renvoie "false" si elle scanne tout le chemin vers la droite sans reconnaître un mot. Sinon, renvoie le mot trouvé et la liste des mots renvoyés par l'appel récursif.

Exemple: "tableapple". Recherche «tab», puis «saut», mais aucun mot dans «ple». Aucun autre mot dans "leapple". Recherche "table", puis "app". "le" pas un mot, alors essaie la pomme, reconnaît, revient.

Pour durer le plus longtemps possible, continuez, en émettant (plutôt qu'en renvoyant) des solutions correctes; puis, choisissez l'optimum selon n'importe quel critère que vous choisissez (maxmax, minmax, moyenne, etc.)


Bon algorithme, y pensait. unutbu a même écrit le code.
Sergey

@Sergey, la recherche en arrière est un algorithme à temps exponentiel. Qu'y a-t-il de «bon» à ce sujet?
Devin Jeanpierre

1
C'est juste simple, je n'ai pas dit que c'était rapide
Sergey

0

Sur la base de la solution d'Unutbu, j'ai implémenté une version 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;
}

Contribution: "tableapplechairtablecupboard"

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

Contribution: "tableprechaun"

Production: [tab, leprechaun]



0

L'expansion de la suggestion de @ miku d'utiliser a Trie, un append-only Trieest relativement simple à mettre en œuvre dans 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

Nous pouvons ensuite construire un Triedictionnaire basé sur un ensemble de mots:

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

Ce qui produira un arbre qui ressemble à ceci ( *indique le début ou la fin d'un mot):

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

Nous pouvons l'incorporer dans une solution en la combinant avec une heuristique sur la façon de choisir les mots. Par exemple, nous pouvons préférer des mots plus longs aux mots plus courts:

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

Nous pouvons utiliser cette fonction comme ceci:

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

Parce que nous maintenons notre position dans le Trieque nous cherchons plus et des mots plus longs, on traverse le trieau plus une fois par solution possible (plutôt que les 2temps pour peanut: pea, peanut). Le court-circuit final nous évite de marcher dans la corde dans le pire des cas.

Le résultat final n'est qu'une poignée d'inspections:

'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 avantage de cette solution réside dans le fait que vous savez très rapidement si des mots plus longs existent avec un préfixe donné, ce qui évite d'avoir à tester de manière exhaustive les combinaisons de séquences par rapport à un dictionnaire. Cela permet également d'accéder à ununsolvable réponse relativement peu coûteuse par rapport aux autres implémentations.

Les inconvénients de cette solution sont une grande empreinte mémoire pour le trieet le coût de la construction trieinitiale.


0

Si vous avez une liste exhaustive des mots contenus dans la chaîne:

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

Utilisation de la compréhension de liste pour parcourir la liste pour localiser le mot et combien de fois il apparaît.

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 fonction renvoie une stringsortie de mots dans l'ordre de la listetable table apple chair cupboard


0

Merci beaucoup pour l'aide sur https://github.com/keredson/wordninja/

Une petite contribution de la même chose en Java de ma part.

La méthode publique splitContiguousWordspourrait être intégrée avec les 2 autres méthodes de la classe ayant ninja_words.txt dans le même répertoire (ou modifiée selon le choix du codeur). Et la méthode splitContiguousWordspourrait être utilisée à cette fin.

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

et si nous n'avons pas de liste de mots?
shirazy

Si j'ai bien compris la requête: Par conséquent, dans l'approche ci-dessus, la publicméthode accepte une phrase de type Stringqui est divisée en fonction d'un premier niveau avec regex. Et pour la liste, ninja_wordsil est disponible en téléchargement à partir du repo git.
Arnab Das


-1

Vous devez identifier votre vocabulaire - peut-être que n'importe quelle liste de mots gratuite fera l'affaire.

Une fois terminé, utilisez ce vocabulaire pour créer une arborescence de suffixes et faites correspondre votre flux d'entrée à celui-ci: http://en.wikipedia.org/wiki/Suffix_tree


Comment cela fonctionnerait-il en pratique? Après avoir construit l'arborescence des suffixes, comment sauriez-vous à quoi faire correspondre?
John Kurlak

@JohnKurlak Comme tout autre automate fini déterministe - la fin d'un mot complet est un état acceptant.
Marcin

Cette approche ne nécessite-t-elle pas un retour en arrière? Vous n'avez pas mentionné le retour en arrière dans votre réponse ...
John Kurlak

Pourquoi pas? Que se passe-t-il si vous avez "tableprechaun", comme mentionné ci-dessous? Il correspondra au mot le plus long possible, "table", et il ne trouvera pas d'autre mot. Il devra revenir en arrière sur "tab" puis correspondre à "leprechaun".
John Kurlak

@JohnKurlak Plusieurs «branches» peuvent être actives en même temps. En effet, vous insérez un jeton dans l'arborescence pour chaque lettre qui est un début de mot possible, et la même lettre peut avancer d'autres jetons actifs.
Marcin
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.