Comment puis-je diviser plusieurs mots joints?
J'ai un tableau de 1000 entrées, avec des exemples ci-dessous:
wickedweather
liquidweather
driveourtrucks
gocompact
slimprojector
Je voudrais pouvoir les diviser en leurs mots respectifs, comme:
wicked weather
liquid weather
drive our trucks
go compact
slim projector
J'espérais une expression régulière Mon faire l'affaire. Mais, comme il n'y a pas de limite pour s'arrêter, il n'y a pas non plus de capitalisation sur laquelle je pourrais éventuellement m'appuyer, je pense qu'une sorte de référence à un dictionnaire pourrait être nécessaire?
Je suppose que cela pourrait être fait à la main, mais pourquoi quand il peut être fini avec le code! =) Mais ce qui a déconcerté les moi. Des idées?
8 réponses
Un humain peut-il le faire?
farsidebag far sidebag farside bag far side bag
Non seulement vous devez utiliser un dictionnaire, mais vous devrez peut-être utiliser une approche statistique pour déterminer ce qui est le plus probable (ou, à Dieu ne plaise, un HMM réel pour votre langage humain de choix...)
Pour savoir comment faire des statistiques qui pourraient être utiles, je vous tourne vers le Dr Peter Norvig, qui aborde un problème différent, mais connexe de la vérification orthographique dans 21 lignes de code : http://norvig.com/spell-correct.html
(il triche un peu en pliant chaque boucle for en une seule ligne.. mais encore).
Mise à jour cela est resté coincé dans ma tête, donc je devais la naissance aujourd'hui. Ce code fait une Division similaire à celle décrite par Robert Gamble, mais il ordonne ensuite les résultats en fonction de la fréquence des mots dans le fichier de dictionnaire fourni (qui devrait maintenant être un texte représentatif de votre domaine ou de l'anglais en général. J'ai utilisé de gros.txt de Norvig, lié ci-dessus, et catted un dictionnaire à elle, à couvrir mots manquants).
Une combinaison de deux mots sera la plupart du temps battre une combinaison de 3 mots, à moins que la différence de fréquence est énorme.
J'ai posté ce code avec quelques modifications mineures sur mon blog
Http://squarecog.wordpress.com/2008/10/19/splitting-words-joined-into-a-single-string/ et a également écrit un peu sur le bug underflow dans ce code.. J'ai été tenté de le réparer tranquillement, mais j'ai pensé que cela pourrait aider certaines personnes qui Je n'ai jamais vu le tour du journal avant: http://squarecog.wordpress.com/2009/01/10/dealing-with-underflow-in-joint-probability-calculations/
Sortie sur vos mots, plus quelques-uns des miens - remarquez ce qui se passe avec "orcore":
perl splitwords.pl big.txt words answerveal: 2 possibilities - answer veal - answer ve al wickedweather: 4 possibilities - wicked weather - wicked we at her - wick ed weather - wick ed we at her liquidweather: 6 possibilities - liquid weather - liquid we at her - li quid weather - li quid we at her - li qu id weather - li qu id we at her driveourtrucks: 1 possibilities - drive our trucks gocompact: 1 possibilities - go compact slimprojector: 2 possibilities - slim projector - slim project or orcore: 3 possibilities - or core - or co re - orc ore
Code:
#!/usr/bin/env perl
use strict;
use warnings;
sub find_matches($);
sub find_matches_rec($\@\@);
sub find_word_seq_score(@);
sub get_word_stats($);
sub print_results($@);
sub Usage();
our(%DICT,$TOTAL);
{
my( $dict_file, $word_file ) = @ARGV;
($dict_file && $word_file) or die(Usage);
{
my $DICT;
($DICT, $TOTAL) = get_word_stats($dict_file);
%DICT = %$DICT;
}
{
open( my $WORDS, '<', $word_file ) or die "unable to open $word_file\n";
foreach my $word (<$WORDS>) {
chomp $word;
my $arr = find_matches($word);
local $_;
# Schwartzian Transform
my @sorted_arr =
map { $_->[0] }
sort { $b->[1] <=> $a->[1] }
map {
[ $_, find_word_seq_score(@$_) ]
}
@$arr;
print_results( $word, @sorted_arr );
}
close $WORDS;
}
}
sub find_matches($){
my( $string ) = @_;
my @found_parses;
my @words;
find_matches_rec( $string, @words, @found_parses );
return @found_parses if wantarray;
return \@found_parses;
}
sub find_matches_rec($\@\@){
my( $string, $words_sofar, $found_parses ) = @_;
my $length = length $string;
unless( $length ){
push @$found_parses, $words_sofar;
return @$found_parses if wantarray;
return $found_parses;
}
foreach my $i ( 2..$length ){
my $prefix = substr($string, 0, $i);
my $suffix = substr($string, $i, $length-$i);
if( exists $DICT{$prefix} ){
my @words = ( @$words_sofar, $prefix );
find_matches_rec( $suffix, @words, @$found_parses );
}
}
return @$found_parses if wantarray;
return $found_parses;
}
## Just a simple joint probability
## assumes independence between words, which is obviously untrue
## that's why this is broken out -- feel free to add better brains
sub find_word_seq_score(@){
my( @words ) = @_;
local $_;
my $score = 1;
foreach ( @words ){
$score = $score * $DICT{$_} / $TOTAL;
}
return $score;
}
sub get_word_stats($){
my ($filename) = @_;
open(my $DICT, '<', $filename) or die "unable to open $filename\n";
local $/= undef;
local $_;
my %dict;
my $total = 0;
while ( <$DICT> ){
foreach ( split(/\b/, $_) ) {
$dict{$_} += 1;
$total++;
}
}
close $DICT;
return (\%dict, $total);
}
sub print_results($@){
#( 'word', [qw'test one'], [qw'test two'], ... )
my ($word, @combos) = @_;
local $_;
my $possible = scalar @combos;
print "$word: $possible possibilities\n";
foreach (@combos) {
print ' - ', join(' ', @$_), "\n";
}
print "\n";
}
sub Usage(){
return "$0 /path/to/dictionary /path/to/your_words";
}
L'algorithme Viterbi est beaucoup plus rapide. Il calcule les mêmes scores que la recherche récursive dans la réponse de Dmitry ci-dessus, mais en temps O (N). (La recherche de Dmitry prend du temps exponentiel; Viterbi le fait par programmation dynamique.)
import re
from collections import Counter
def viterbi_segment(text):
probs, lasts = [1.0], [0]
for i in range(1, len(text) + 1):
prob_k, k = max((probs[j] * word_prob(text[j:i]), j)
for j in range(max(0, i - max_word_length), i))
probs.append(prob_k)
lasts.append(k)
words = []
i = len(text)
while 0 < i:
words.append(text[lasts[i]:i])
i = lasts[i]
words.reverse()
return words, probs[-1]
def word_prob(word): return dictionary[word] / total
def words(text): return re.findall('[a-z]+', text.lower())
dictionary = Counter(words(open('big.txt').read()))
max_word_length = max(map(len, dictionary))
total = float(sum(dictionary.values()))
Tester:
>>> viterbi_segment('wickedweather')
(['wicked', 'weather'], 5.1518198982768158e-10)
>>> ' '.join(viterbi_segment('itseasyformetosplitlongruntogetherblocks')[0])
'its easy for me to split long run together blocks'
Pour être pratique, vous voudrez probablement quelques améliorations:
- ajoutez des journaux de probabilités, ne multipliez pas les probabilités. Cela évite le sous-débit en virgule flottante.
- vos entrées seront en général utilisées mots qui ne sont pas dans votre corpus. Ces sous-chaînes doivent recevoir une probabilité non nulle en tant que mots, ou vous vous retrouvez avec aucune solution ou une mauvaise solution. (C'est tout aussi vrai pour l'algorithme de recherche exponentielle ci-dessus.) Cette probabilité doit être siphonnée des probabilités des mots du corpus et distribuée de manière plausible entre tous les autres candidats de mots: le sujet général est connu sous le nom de lissage dans les modèles de langage statistique. (Vous pouvez vous en sortir avec des hacks assez rugueux, cependant.) C'est là que le O (N) Viterbi algorithme souffle l'algorithme de recherche, parce que considérer les mots non-corpus explose le facteur de ramification.
Le meilleur outil pour le travail ici est la récursivité, pas les expressions régulières. L'idée de base est de commencer par le début de la chaîne à la recherche d'un mot, puis de prendre le reste de la chaîne et de chercher un autre mot, et ainsi de suite jusqu'à la fin de la chaîne. Une solution récursive est naturelle car le retour en arrière doit se produire lorsqu'un reste donné de la chaîne ne peut pas être divisé en un ensemble de mots. La solution ci-dessous utilise un dictionnaire pour déterminer ce qu'est un mot, et imprime solutions telles qu'elles les trouvent (certaines chaînes peuvent être divisées en plusieurs ensembles de mots possibles, par exemple wickedweather pourrait être analysé comme "wicked we at her"). Si vous voulez juste un ensemble de mots, vous devrez déterminer les règles pour sélectionner le meilleur ensemble, peut-être en sélectionnant la solution avec le moins de mots ou en définissant une longueur de mot minimale.
#!/usr/bin/perl
use strict;
my $WORD_FILE = '/usr/share/dict/words'; #Change as needed
my %words; # Hash of words in dictionary
# Open dictionary, load words into hash
open(WORDS, $WORD_FILE) or die "Failed to open dictionary: $!\n";
while (<WORDS>) {
chomp;
$words{lc($_)} = 1;
}
close(WORDS);
# Read one line at a time from stdin, break into words
while (<>) {
chomp;
my @words;
find_words(lc($_));
}
sub find_words {
# Print every way $string can be parsed into whole words
my $string = shift;
my @words = @_;
my $length = length $string;
foreach my $i ( 1 .. $length ) {
my $word = substr $string, 0, $i;
my $remainder = substr $string, $i, $length - $i;
# Some dictionaries contain each letter as a word
next if ($i == 1 && ($word ne "a" && $word ne "i"));
if (defined($words{$word})) {
push @words, $word;
if ($remainder eq "") {
print join(' ', @words), "\n";
return;
} else {
find_words($remainder, @words);
}
pop @words;
}
}
return;
}
Je pense que vous avez raison de penser que ce n'est pas vraiment un travail pour une expression régulière. J'aborderais cela en utilisant l'idée du dictionnaire-recherchez le préfixe le plus long qui est un mot dans le dictionnaire. Lorsque vous trouvez cela, coupez-le et faites de même avec le reste de la chaîne.
La méthode ci-dessus est sujette à ambiguïté, par exemple "drivereallyfast" trouverait d'abord "driver" et aurait ensuite des problèmes avec "eallyfast". Donc, vous devrez également faire un retour en arrière si vous avez rencontré cette situation. Ou, puisque vous n'avez pas beaucoup de chaînes à diviser, faites simplement à la main celles qui échouent à la division automatisée.
Eh bien, le problème lui-même n'est pas résolu avec juste une expression régulière. Une solution (probablement pas la meilleure) serait d'obtenir un dictionnaire et de faire une correspondance d'expression régulière pour chaque travail dans le dictionnaire à chaque mot de la liste, en ajoutant l'Espace chaque fois que c'est réussi. Certes, ce ne serait pas terriblement rapide, mais il serait facile à programmer et plus rapide que la main le faire.
Une solution basée sur un dictionnaire serait nécessaire. Cela pourrait être simplifié quelque peu si vous avez un dictionnaire limité de mots qui peuvent se produire, sinon les mots qui forment le préfixe d'autres mots vont être un problème.
Ceci est lié à un problème connu sous le nom de identifiant fractionnement ou identifiant nom tokenization. Dans le cas de L'OP, les entrées semblent être des concaténations de mots ordinaires; dans le fractionnement des identifiants, les entrées sont des noms de classe, des noms de fonction ou d'autres identificateurs du code source, et le problème est plus difficile. Je me rends compte que c'est une vieille question et que le PO a résolu leur problème ou est passé à autre chose, mais au cas où quelqu'un d'autre rencontrerait cette question en cherchant identifiant splitters (comme je l'étais, il n'y a pas longtemps), je voudrais offrir spirale ("diviseurs pour les identificateurs: une bibliothèque "). Il est écrit en Python mais est livré avec un utilitaire de ligne de commande qui peut lire un fichier d'identifiants (un par ligne) et diviser chacun.
Diviser les identifiants est trompeusement difficile. Les programmeurs utilisent généralement des abréviations, des acronymes et des fragments de mots lorsqu'ils nomment des choses, et ils n'utilisent pas toujours des conventions cohérentes. Même quand les identificateurs suivent certaines conventions telles que le cas des chameaux, des ambiguïtés peuvent survenir.
Spiral implémente de nombreux algorithmes de fractionnement d'identifiants, y compris un nouvel algorithme appelé Ronin. Il utilise une variété de règles heuristiques, de dictionnaires anglais et de tables de fréquences de jetons obtenues à partir de dépôts de code source miniers. Ronin peut diviser des identifiants qui n'utilisent pas camel case ou d'autres conventions de nommage, y compris des cas tels que le fractionnement de J2SEProjectTypeProfiler
en [J2SE
, Project
, Type
, Profiler
], ce qui oblige le lecteur à reconnaître J2SE
comme une unité. Voici quelques autres exemples de ce que Ronin peut diviser:
# spiral mStartCData nonnegativedecimaltype getUtf8Octets GPSmodule savefileas nbrOfbugs
mStartCData: ['m', 'Start', 'C', 'Data']
nonnegativedecimaltype: ['nonnegative', 'decimal', 'type']
getUtf8Octets: ['get', 'Utf8', 'Octets']
GPSmodule: ['GPS', 'module']
savefileas: ['save', 'file', 'as']
nbrOfbugs: ['nbr', 'Of', 'bugs']
En utilisant les exemples de la question du PO:
# spiral wickedweather liquidweather driveourtrucks gocompact slimprojector
wickedweather: ['wicked', 'weather']
liquidweather: ['liquid', 'weather']
driveourtrucks: ['driveourtrucks']
gocompact: ['go', 'compact']
slimprojector: ['slim', 'projector']
Comme vous pouvez le voir, ce n'est pas parfait. Il est à noter que Ronin a un certain nombre de paramètres et que leur ajustement permet également de diviser driveourtrucks
, mais au prix d'une détérioration des performances sur les identificateurs de programme.
Plus d'informations peuvent être trouvées dans le dépôt GitHub pour Spirale .
Je peux être downmoded pour cela, mais Demandez à la secrétaire de le faire .
Vous passerez plus de temps sur une solution de dictionnaire qu'il ne faudrait pour traiter manuellement. De plus, vous n'aurez peut-être pas confiance à 100% dans la solution, donc vous devrez quand même lui accorder une attention manuelle.