Как я могу разделить несколько слов? - PullRequest
41 голосов
/ 12 октября 2008

У меня есть массив из 1000 или около того записей, с примерами ниже:

wickedweather
liquidweather
driveourtrucks
gocompact
slimprojector

Я бы хотел разделить их на соответствующие слова:

wicked weather
liquid weather
drive our trucks
go compact
slim projector

Я надеялся, что регулярное выражение поможет мне. Но, поскольку нет границы, на которой можно остановиться, и нет ли какой-либо заглавной буквы, на которой я мог бы остановиться, я думаю, что какая-то ссылка на словарь может быть необходима?

Полагаю, это можно сделать вручную, но почему - когда это можно сделать с помощью кода! =) Но это поставило меня в тупик. Есть идеи?

Ответы [ 9 ]

70 голосов
/ 27 января 2009

Алгоритм Витерби намного быстрее. Он вычисляет те же оценки, что и рекурсивный поиск в ответе Дмитрия выше, но за O (n) раз. (Поиск Дмитрия занимает экспоненциальное время; Витерби - динамическим программированием.)

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

Тестирование:

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

Чтобы быть практичным, вы, вероятно, захотите пару уточнений:

  • Добавьте журналы вероятностей, не умножайте вероятности. Это позволяет избежать потери значения с плавающей запятой.
  • Ваши входные данные, как правило, будут использовать слова, отсутствующие в вашем корпусе. Этим подстрокам должна быть назначена ненулевая вероятность в виде слов, иначе у вас не будет решения или плохого решения. (Это так же верно для приведенного выше алгоритма экспоненциального поиска.) Эта вероятность должна быть откачана от вероятностей корпусных слов и достоверно распределена среди всех других слов-кандидатов: общая тема известна как сглаживание в статистических языковых моделях. (Тем не менее, вы можете избежать некоторых довольно грубых взломов.) Именно здесь алгоритм O (n) Витерби срывает алгоритм поиска, потому что рассмотрение слов без корпусов увеличивает фактор ветвления.
33 голосов
/ 12 октября 2008

Может ли человек сделать это?

farsidebag
far sidebag
farside bag
far side bag

Мало того, что вы должны использовать словарь, вам, возможно, придется использовать статистический подход, чтобы выяснить, что наиболее вероятно (или, не дай бог, фактический HMM для вашего человеческого языка по вашему выбору ...)

Для получения статистических данных, которые могут быть полезны, я обращаюсь к доктору Питеру Норвигу, который решает другую, но связанную с этим проблему проверки орфографии в 21 строке кода : http://norvig.com/spell-correct.html

(он немного обманывает, складывая каждый цикл for в одну строку ... но все же).

Обновление Это застряло в моей голове, поэтому мне пришлось родить его сегодня. Этот код выполняет разделение, аналогичное описанному Робертом Гэмблом, но затем он упорядочивает результаты на основе частоты слов в предоставленном файле словаря (который теперь, как ожидается, будет некоторым текстовым представителем вашего домена или английского языка в целом. Я использовал большой .txt от Norvig, ссылка на которую приведена выше, и содержит словарь для покрытия пропущенных слов).

Комбинация из двух слов в большинстве случаев побьет комбинацию из 3 слов, если разница в частотах не огромна.


Я разместил этот код с некоторыми незначительными изменениями в своем блоге

http://squarecog.wordpress.com/2008/10/19/splitting-words-joined-into-a-single-string/ и также написал немного об ошибке недопустимости в этом коде. Я хотел просто спокойно ее исправить, но подумал, что это может помочь некоторым людям, которые раньше не видели трюк с журналом: http://squarecog.wordpress.com/2009/01/10/dealing-with-underflow-in-joint-probability-calculations/


Вывод ваших слов, плюс несколько моих собственных - обратите внимание, что происходит с "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

Код:

#!/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";
}
8 голосов
/ 12 октября 2008

Лучший инструмент для работы здесь - это рекурсия, а не регулярные выражения. Основная идея состоит в том, чтобы начать с начала строки в поисках слова, затем взять остаток строки и искать другое слово и так далее, пока не будет достигнут конец строки. Рекурсивное решение является естественным, так как возврат должен происходить, когда заданный остаток строки не может быть разбит на набор слов. Приведенное ниже решение использует словарь, чтобы определить, что такое слово, и выводит решения по мере их нахождения (некоторые строки можно разбить на несколько возможных наборов слов, например, wickedweather может быть проанализирован как «злые мы на нее»). Если вам нужен только один набор слов, вам необходимо определить правила выбора наилучшего набора, возможно, выбрав решение с наименьшим количеством слов или установив минимальную длину слова.

#!/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;
}
3 голосов
/ 12 октября 2008

Я думаю, вы правы, полагая, что это не совсем работа для регулярного выражения. Я бы подошел к этому, используя идею словаря - ищите самый длинный префикс, который является словом в словаре. Когда вы найдете это, отрежьте его и сделайте то же самое с остальной частью строки.

Описанный выше метод подвержен неоднозначности, например, «drivereallyfast» сначала найдет «driver», а затем возникнет проблема с «eallyfast». Таким образом, вы также должны были бы вернуться назад, если бы столкнулись с этой ситуацией. Или, поскольку у вас не так много строк для разделения, просто сделайте вручную те, которые потерпели неудачу в автоматическом разделении.

1 голос
/ 23 марта 2018

Это связано с проблемой, известной как разделение идентификатора или идентификация имени токена . В случае ФП входные данные кажутся связями обычных слов; при разделении идентификаторов входными данными являются имена классов, имена функций или другие идентификаторы из исходного кода, и проблема сложнее. Я понимаю, что это старый вопрос, и ОП либо решил их проблему, либо пошел дальше, но в случае, если кто-то еще сталкивался с этим вопросом при поиске разделителей идентификаторов (как я это делал не так давно), я хотел бы предложить Спираль (" Разделители для Идентификаторов: Библиотека "). Он написан на Python, но поставляется с утилитой командной строки, которая может читать файл идентификаторов (по одному на строку) и разбивать каждый из них.

Разделение идентификаторов обманчиво сложно. Программисты обычно используют аббревиатуры, сокращения и фрагменты слов при именовании вещей, и они не всегда используют согласованные соглашения. Даже в тех случаях, когда идентификаторы следуют некоторому соглашению, такому как случай верблюда, могут возникнуть неоднозначности.

Спираль реализует многочисленные алгоритмы разделения идентификаторов, включая новый алгоритм под названием Ronin. Он использует различные эвристические правила, английские словари и таблицы частот токенов, полученных из хранилищ исходного кода майнинга. Ронин может разделять идентификаторы, которые не используют регистр верблюдов или другие соглашения об именах, включая такие случаи, как разбиение J2SEProjectTypeProfiler на [J2SE, Project, Type, Profiler], что требует от читателя распознавать J2SE как единое целое. Вот еще несколько примеров того, что может разделить Ронин:

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

Используя примеры из вопроса ОП:

# spiral wickedweather liquidweather  driveourtrucks gocompact slimprojector
wickedweather: ['wicked', 'weather']
liquidweather: ['liquid', 'weather']
driveourtrucks: ['driveourtrucks']
gocompact: ['go', 'compact']
slimprojector: ['slim', 'projector']

Как видите, оно не идеально. Стоит отметить, что у Ronin есть ряд параметров, и их настройка позволяет также разделять driveourtrucks, но за счет снижения производительности по идентификаторам программ.

Дополнительную информацию можно найти в репозитории GitHub для Spiral .

1 голос
/ 12 октября 2008

Требуется решение на основе словаря. Это может быть несколько упрощено, если у вас есть ограниченный словарь слов, которые могут встречаться, в противном случае слова, которые образуют префикс других слов, будут проблемой.

1 голос
/ 12 октября 2008

Ну, сама проблема не решаема только с помощью регулярного выражения. Решение (возможно, не лучшее) состоит в том, чтобы получить словарь и сопоставить регулярное выражение для каждой работы в словаре с каждым словом в списке, добавляя пробел в случае успеха. Конечно, это будет не очень быстро, но это будет легко программировать и быстрее, чем делать это вручную.

0 голосов
/ 16 января 2019

Существует выпущенный пакет python Santhosh, называемый mlmorph, который можно использовать для морфологического анализа.

https://pypi.org/project/mlmorph/

Примеры:

from mlmorph import Analyser
analyser = Analyser()
analyser.analyse("കേരളത്തിന്റെ")

Придает

[('കേരളം<np><genitive>', 179)]

Он также написал блог на тему https://thottingal.in/blog/2017/11/26/towards-a-malayalam-morphology-analyser/

0 голосов
/ 12 октября 2008

Меня могут унизить для этого, но пусть секретарь сделает это .

Вы потратите больше времени на решение для словаря, чем на ручную обработку. Кроме того, у вас не будет 100% уверенности в решении, поэтому вам все равно придется все равно уделить ему внимание.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...