2008-10-12 14 views
37

I tienen una serie de 1000 o menos entradas, con ejemplos a continuación:¿Cómo puedo dividir varias palabras combinadas?

wickedweather 
liquidweather 
driveourtrucks 
gocompact 
slimprojector 

I gustaría ser capaz de dividir estos en sus respectivas palabras, como:

wicked weather 
liquid weather 
drive our trucks 
go compact 
slim projector 

que estaba esperando una expresión regular mi hacer el truco. Pero, dado que no hay límite para detenerse, ni hay ningún tipo de capitalización que pueda activar, estoy pensando que podría ser necesario algún tipo de referencia a un diccionario.

Supongo que podría hacerse a mano, pero ¿por qué? ¡Cuando se puede hacer con código! =) Pero esto me ha dejado perplejo. ¿Algunas ideas?

+2

Tenga en cuenta que una implementación ingenua devolvería "wick ed weather" –

+0

hey soluciones óptimas, vi su respuesta en una pregunta EMR y me preguntaba si podría contactarlo con algunas preguntas sobre TI sanitaria. –

Respuesta

32

¿Puede un humano hacerlo?

 
farsidebag 
far sidebag 
farside bag 
far side bag 

No sólo usted tiene que usar un diccionario, puede que tenga que utilizar un enfoque estadístico para averiguar lo que es más probable (o, Dios no lo quiera, un HMM real de su lenguaje humano de elección ...)

para saber cómo hacer estadísticas que podrían ser útiles, que a su vez a Dr. Peter Norvig, que se dirige a un problema diferente, pero relacionado de corrección ortográfica en 21 líneas de código: http://norvig.com/spell-correct.html

(he hace trampa un poco doblando cada bucle en una sola línea ... pero aún así).

actualización Esto quedó atascado en mi cabeza, así que tuve que nacer hoy en día.Este código realiza una división similar a la descrita por Robert Gamble, pero luego ordena los resultados en función de la frecuencia de las palabras en el archivo de diccionario proporcionado (que ahora se espera que sea un texto representativo de su dominio o inglés en general. .txt de Norvig, vinculado anteriormente, y le echó un diccionario, para cubrir palabras faltantes).

Una combinación de dos palabras la mayoría de las veces superará una combinación de 3 palabras, a menos que la diferencia de frecuencia sea enorme.


he publicado este código con algunos cambios de menor importancia en mi blog

http://squarecog.wordpress.com/2008/10/19/splitting-words-joined-into-a-single-string/ y también escribió un poco sobre el error de desbordamiento en este código .. tuve la tentación de simplemente en silencio arreglarlo, pero pensó que esto puede ayudar a algunas personas que no han visto el truco de registro antes: http://squarecog.wordpress.com/2009/01/10/dealing-with-underflow-in-joint-probability-calculations/


de salida en sus palabras, además de unos cuantos de mi propia - Anuncio de lo que sucede con "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 

Código:

#!/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([email protected]); 
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([email protected]){ 
    #('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"; 
} 
+0

¿Se puede ejecutar esto en Windows XP? ¿Cómo puedo cargar Perl? ¡Obviamente necesito salir más (en términos de otros idiomas)! :) – Taptronic

+1

Sí, estás buscando algo llamado ActivePerl, que es la distribución de Windows. No usé ningún módulo, por lo que no es necesario que agregue nada a la compilación estándar. Solo encuentra un buen diccionario representativo. – SquareCog

+1

+1 - No conozco a Perl pero te di +1 por ir más allá del llamado del deber. ¡Bonito! –

3

Creo que tienes razón al pensar que no es realmente un trabajo para una expresión regular. Me acercaría a esto usando la idea del diccionario: busque el prefijo más largo que es una palabra en el diccionario. Cuando lo encuentres, córtalo y haz lo mismo con el resto de la cadena.

El método anterior está sujeto a ambigüedad, por ejemplo "drivereallyfast" primero encontraría "driver" y luego tendría problemas con "eallyfast". Así que también tendrías que hacer un poco de retroceso si te topas con esta situación. O bien, dado que no tiene tantas cadenas para dividir, simplemente haga a mano las que fallan en la división automática.

+1

Tengo que encontrar un archivo de diccionario para golpear. – Taptronic

+2

http://www.freebsd.org/cgi/cvsweb.cgi/src/share/dict/web2?rev=1.12 –

+0

¡Gracias! Voy a conseguir esto y ese Perl juntos, vean qué pasa. – Taptronic

1

Bueno, el problema en sí mismo no se puede resolver con solo una expresión regular. Una solución (probablemente no la mejor) sería obtener un diccionario y hacer una coincidencia de expresión regular para cada trabajo en el diccionario con cada palabra en la lista, agregando el espacio cada vez que sea exitoso. Ciertamente, esto no sería terriblemente rápido, pero sería fácil de programar y más rápido que hacerlo manualmente.

1

Se necesitaría una solución basada en diccionario. Esto podría simplificarse un poco si tiene un diccionario limitado de palabras que puede ocurrir, de lo contrario las palabras que forman el prefijo de otras palabras van a ser un problema.

0

Puedo obtener downmodded para esto, pero hacer que la secretaria lo haga.

Pasarás más tiempo en una solución de diccionario de lo que te llevaría procesar manualmente. Además, posiblemente no tendrá una confianza del 100% en la solución, por lo que igual tendrá que darle atención manual.

+2

hombre ... ¡ahora realmente quiero devolverte! :-) Intentamos un enfoque similar para filtrar consultas de búsqueda traviesas alguna vez .. Pasé más tiempo construyendo una interfaz agradable que usaría una secretaria (persona de relaciones públicas, en mi caso), que usaría en un clasificador. – SquareCog

7

La mejor herramienta para el trabajo aquí es la recursividad, no expresiones regulares. La idea básica es comenzar desde el principio de la cuerda buscando una palabra, luego tomar el resto de la cuerda y buscar otra palabra, y así sucesivamente hasta que se llegue al final de la cuerda. Una solución recursiva es natural, ya que el retroceso debe suceder cuando un resto determinado de la cadena no se puede dividir en un conjunto de palabras. La siguiente solución usa un diccionario para determinar qué es una palabra e imprime las soluciones a medida que las encuentra (algunas cadenas se pueden dividir en múltiples conjuntos de palabras posibles, por ejemplo, wickedweather podría analizarse como "malvados en ella"). Si solo desea un conjunto de palabras, necesitará determinar las reglas para seleccionar el mejor conjunto, tal vez seleccionando la solución con el menor número de palabras o estableciendo una longitud de palabra mínima.

#!/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; 
} 
+0

no lo han ejecutado, pero se ve como una mejor solución que BKB ya que produce todas las posibilidades. – SquareCog

+0

Esto funciona como magia. Exactamente lo que he estado buscando, muchas gracias. Estoy tratando de traducir a PHP. Si hay una versión de PHP, por favor compártala aquí. – Mani

56

El Viterbi algorithm es mucho más rápido. Calcula los mismos puntajes que la búsqueda recursiva en la respuesta de Dmitry anterior, pero en O (n) tiempo. (Búsqueda de Dmitry lleva tiempo exponencial; Viterbi hace por programación dinámica.)

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

probándola:

>>> viterbi_segment('wickedweather') 
(['wicked', 'weather'], 5.1518198982768158e-10) 
>>> ' '.join(viterbi_segment('itseasyformetosplitlongruntogetherblocks')[0]) 
'its easy for me to split long run together blocks' 

para ser práctico es probable que desee un par de mejoras:

  • Agregue registros de probabilidades, no multiplique las probabilidades. Esto evita el underflow del punto flotante.
  • Sus entradas en general utilizarán palabras que no están en su corpus. A estas subcadenas se les debe asignar una probabilidad distinta de cero como palabras, o terminas sin solución o una mala solución. (Esto es igualmente cierto para el algoritmo de búsqueda exponencial anterior). Esta probabilidad tiene que desviarse de las probabilidades de las palabras del corpus y distribuirse de manera plausible entre todas las demás palabras candidatas: el tema general se conoce como suavizado en modelos de lenguaje estadístico. (Sin embargo, puedes salirte con algunos trucos bastante toscos). Aquí es donde el algoritmo de O (n) Viterbi elimina el algoritmo de búsqueda, porque considerar las palabras que no son del corpus hace explotar el factor de ramificación.
+0

¡Bien hecho! También es un buen punto acerca del suavizado. – SquareCog

+0

¿No es ese el algoritmo utilizado para ordenar las secuencias de ADN? – wisty

+0

No lo sé, pero la idea general de Viterbi (encontrar la secuencia más probable de estados ocultos dada una secuencia de observaciones) - eso también debería tener usos con el ADN. –

Cuestiones relacionadas