Entrée:"tableapplechairtablecupboard..."
Plusieurs mots
Quel serait un algorithme efficace pour diviser un tel texte en liste de mots et obtenir:
Sortie:["table", "Apple", "chair", "table", ["cupboard", ["cup", "board"]], ...]
La première chose qui 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)
P.S.
Nous avons une liste de tous les mots possibles.
Le mot "armoire" peut être "tasse" et "planche", sélectionnez la plus longue.
Langue: python, mais l'essentiel est l'algorithme lui-même.
Un algorithme naïf ne donnera pas de bons résultats lorsqu'il est appliqué à des données réelles. Voici un algorithme de 20 lignes qui exploite la fréquence relative des mots pour donner des résultats précis pour du texte réel.
(Si vous voulez une réponse à votre question d'origine qui n'utilise pas la fréquence Word, vous devez affiner ce que l'on entend exactement par "mot le plus long": est-il préférable d'avoir un mot de 20 lettres et dix de 3 lettres mots, ou est-il préférable d'avoir cinq mots de 10 lettres? Une fois que vous vous êtes fixé une définition précise, il vous suffit de changer la ligne définissant wordcost
pour refléter la signification voulue.)
La meilleure façon de procéder est de modèle la distribution de la sortie. Une bonne première approximation consiste à supposer que tous les mots sont distribués indépendamment. Il vous suffit alors 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 le 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.
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))
J'utilise ce dictionnaire 125k Word rapide et sale que j'ai mis en place à partir d'un petit sous-ensemble de Wikipedia.
Avant: thumbgreenappleactiveassignmentweeklymetaphor.
Après: thumb green Apple métaphore hebdomadaire d'affectation active.
Avant: il y adesinformations textuellesdespersonnes commentéesàpartirdehtmlmaisd'autrespersonnalisentdescaractèresdélimitéspeubledanslesexempleséchantillonnelappliqueractivationdel'affectationdepuisune semaineestapho-brusquementlorsqu'ilestréculierpourvecherestrastestherthestaresthefthestaresthefthedestarelestrastedestrustestareleconstaurer.
Après: il y a des masses d'informations textuelles des commentaires des gens qui sont analysées à partir de html mais il n'y a pas de caractères délimités en eux par exemple thumb green Apple métaphore hebdomadaire d'affectation active apparemment il y a le pouce vert Apple etc dans la chaîne j'ai également un grand dictionnaire pour demander si le mot est raisonnable donc quel est le moyen d'extraction le plus rapide Merci beaucoup.
Avant: il était obscur et ténèbrespourlesenvahirouexceptatàdesintervalles occasionnelsquand il a été contrôlépar une rafaleviolentequi a balayélesesthésiquespour lesfamillespourlesnouveauxcontenuscélébritésenflamantenflamantenflamantenflamantenflamantenfamille.
Après: ce fut une nuit sombre et orageuse la pluie tomba en torrents sauf à intervalles occasionnels quand elle fut contrôlée par une violente rafale de vent qui balaya la car c'est à Londres que notre scène traîne le long des toits et agite farouchement la faible flamme des lampes qui luttent contre l'obscurité.
Comme vous pouvez le voir, il est essentiellement parfait. 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.
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 Word pour réduire la taille de l'ensemble des 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 réduira l'utilisation de la mémoire au minimum et n'aura presque certainement aucun effet sur la qualité.
Sur la base de l'excellent travail dans le top answer , j'ai créé un package pip
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 un list
plutôt qu'un str
, cela fonctionne dans python3
, il inclut la liste de mots et se divise correctement même s'il existe des caractères non alpha (comme les traits de soulignement, les tirets, etc.).
Merci encore à Generic Human!
Voici une 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'])))
les rendements
['table', 'Apple', 'chair', 'table', 'cupboard']
['tab', 'leprechaun']
En utilisant un triestructure de données , qui contient la liste des mots possibles, il ne serait pas trop compliqué de faire ce qui suit:
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 présente l'inconvénient de nécessiter des fréquences Word. Ne convient pas à tous les cas d'utilisation.
Voici une solution simple utilisant un algorithme Divide and Conquer .
find_words('cupboard')
renverra ['cupboard']
plutôt que ['cup', 'board']
(en supposant que cupboard
, cup
et board
sont dans le dictionnaire )find_words('charactersin')
pourrait retourner ['characters', 'in']
ou peut-être retournera ['character', 'sin']
(comme vu ci-dessous). Vous pouvez assez facilement modifier l'algorithme pour retourner toutes les solutions optimales.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 de 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écher par exemple pouce vert Apple métaphore hebdomadaire d'affectation active apparemment il y a pouce vert Apple etc dans la chaîne j'ai également un grand dictionnaire pour demander si le mot est raisonnable, donc quel est le moyen d'extraction le plus rapide
La réponse de https://stackoverflow.com/users/1515832/generic-human est excellente. Mais la meilleure mise en œuvre de ce 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 de non-bigramme de partage 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 bigram, vous pouvez évaluer p ("asseoir") comme un bigram vs p ("asseoir") et l'ancien gagne. Fondamentalement, 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 l'un après l'autre. Malheureusement, ce sont aussi les mots qui sont souvent collés ensemble dans de nombreux cas et confond le séparateur.
Voici le lien vers les données (ce sont des données pour 3 problèmes distincts et la segmentation n'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 sont en place 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
Si vous précompilez la liste de mots dans un DFA (qui sera très lent), le temps nécessaire pour faire correspondre une entrée sera proportionnel à la longueur du chaîne (en fait, un peu plus lentement que de simplement parcourir la chaîne).
Il s'agit en fait d'une version plus générale de l'algorithme trie mentionné précédemment. Je ne le mentionne que pour completeless - 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 de simplement jeter les données DFA compilées et recherche NFA.
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();
}
Si vous avez une liste exhaustive des mots contenus dans la chaîne:
Word_list = ["table", "Apple", "chair", "cupboard"]
Utiliser la compréhension de la 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 sortie string
de mots dans l'ordre de la liste table table Apple chair cupboard
Développer sur @ miku's suggestion d'utiliser un Trie
, un ajout uniquement Trie
est relativement simple à implémenter 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 alors construire un dictionnaire basé sur Trie
à partir d'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 à des 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" ]
Comme nous conservons notre position dans le Trie
lorsque nous recherchons des mots de plus en plus longs, nous parcourons le trie
au plus une fois par solution possible (plutôt que 2
fois pour peanut
: pea
, peanut
). Le dernier court-circuit nous évite de marcher dans le pire des cas dans les cordes.
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 rend également l'accès à une réponse unsolvable
relativement bon marché par rapport aux autres implémentations.
Les inconvénients de cette solution sont une grande empreinte mémoire pour le trie
et le coût de construction du trie
à l'avance.
Pour la langue allemande, il y a CharSplit qui utilise l'apprentissage automatique et fonctionne assez bien pour les chaînes de quelques mots.
Basé sur 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;
}
Entrée:"tableapplechairtablecupboard"
Sortie:[table, Apple, chair, table, cupboard]
Entrée:"tableprechaun"
Sortie:[tab, leprechaun]
Il semble qu'un retour en arrière assez banal suffira. 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 "faux" si elle balaye complètement vers la droite sans reconnaître un mot. Sinon, retourne le mot trouvé et la liste des mots retournés par l'appel récursif.
Exemple: "ananas". Trouve "tab", puis "bond", mais pas de mot en "ple". Aucun autre mot dans "leapple". Trouve "table", puis "app". "le" n'est pas un mot, essaie donc Apple, reconnaît, revient.
Pour obtenir le plus de temps possible, continuez, n'émettant (plutôt que renvoyant) des solutions correctes; ensuite, choisissez l'optimal selon le critère que vous choisissez (maxmax, minmax, average, etc.)