web-dev-qa-db-ja.com

複数の結合された単語を分割するにはどうすればよいですか?

以下の例で、1000程度のエントリの配列があります。

wickedweather
liquidweather
driveourtrucks
gocompact
slimprojector

これらをそれぞれの単語に分割できるようにしたいと思います。

wicked weather
liquid weather
drive our trucks
go compact
slim projector

私は正規表現が私のトリックをすることを望んでいました。しかし、立ち止まる境界がなく、キー入力できるような大文字の使用もありませんので、辞書への何らかの参照が必要になるのではないかと考えています。

手作業でできると思いますが、なぜ-コードでできるのか! =)しかし、これは私を困惑させました。何か案は?

45
Taptronic

人間はそれをすることができますか?

ファーサイドバッグ
ファーサイドバッグ
ファーサイドバッグ
ファーサイドバッグ

辞書を使用する必要があるだけでなく、統計的アプローチを使用して、最も可能性が高いものを把握する必要がある場合があります(または、神が禁じている、選択した人間の言語の実際のHMM ...)

役立つ可能性のある統計を行う方法については、21行のスペルチェックの別の、しかし関連する問題に対処するPeterNorvig博士に相談します。コードのhttp://norvig.com/spell-correct.html

(彼はすべてのforループを1行に折りたたむことで少しごまかしますが、それでも)。

Updateこれが頭に残ったので、今日出産しなければなりませんでした。このコードは、Robert Gambleによって説明されたものと同様の分割を行いますが、提供された辞書ファイル(現在、ドメインまたは英語全般を表すテキストであると予想されます)の単語頻度に基づいて結果を並べ替えます。 Norvigの.txtは、上にリンクされており、不足している単語をカバーするために辞書が追加されています)。

頻度の差が大きくない限り、2つの単語の組み合わせは、ほとんどの場合3つの単語の組み合わせを上回ります。


このコードをブログに少し変更して投稿しました

http://squarecog.wordpress.com/2008/10/19/splitting-words-joined-into-a-single-string/ また、このコードのアンダーフローバグについても少し書いています。 。私は静かにそれを修正したくなりましたが、これは以前にログトリックを見たことがない一部の人々に役立つかもしれないと考えました: http://squarecog.wordpress.com/2009/01/10/dealing-with -アンダーフロー-イン-ジョイント-確率-計算/


あなたの言葉に加えて、私自身のいくつかを出力してください-「orcore」で何が起こるかに注意してください:

 Perl splitwords.pl big.txt words 
 answerveal:2つの可能性
-子牛肉に答える
-ve al 
 
 wickedweatherに答える:4つの可能性
-邪悪な天気
-邪悪な私たちを彼女に
-邪悪な天気
-邪悪な私たちを彼女に
 
 liquidweather:6つの可能性
-液体の天気
-液体の私たち彼女
-液体の天気
-li quid we at her 
-li qu id weather 
-liqu id we at her 
 
 driveourtrucks:1可能性
-トラックを運転する
 
 gocompact: 1つの可能性
-コンパクトに
 
 slimprojector:2つの可能性
-スリムなプロジェクター
-スリムなプロジェクトまたは
 
 orcore:3つの可能性
-またはcore 
-またはcore 
-orcore 
 

コード:

#!/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";
}
33
SquareCog

ビタビアルゴリズム ははるかに高速です。上記のDmitryの回答の再帰検索と同じスコアを計算しますが、O(n)時間です。(Dmitryの検索には指数関数的な時間がかかります。Viterbiは動的計画法によって計算します。)

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'

実用的にするには、いくつかの改良が必要になる可能性があります。

  • 確率のログを追加します。確率を乗算しないでください。これにより、浮動小数点のアンダーフローが回避されます。
  • 入力は通常、コーパスにない単語を使用します。これらの部分文字列には、単語としてゼロ以外の確率を割り当てる必要があります。そうしないと、解決策がないか、解決策が悪くなります。 (これは、上記の指数探索アルゴリズムにも当てはまります。)この確率は、コーパスワードの確率から吸い上げられ、他のすべてのWord候補にもっともらしく分散される必要があります。一般的なトピックは統計言語モデルの平滑化として知られています。 (ただし、かなり大まかなハックで回避できます。)ここで、O(n) Viterbiアルゴリズムが検索アルゴリズムを吹き飛ばします。これは、コーパス以外の単語を考慮すると分岐係数が大きくなるためです。 。
73
Darius Bacon

ここでの仕事に最適なツールは、正規表現ではなく再帰です。基本的な考え方は、文字列の最初から単語を探し、次に文字列の残りの部分を取得して別の単語を探すというように、文字列の最後に到達するまで続けます。文字列の特定の残りの部分を単語のセットに分割できない場合にバックトラックを実行する必要があるため、再帰的な解決策は自然です。以下のソリューションは、辞書を使用してWordとは何かを判別し、ソリューションを見つけたときにそれを出力します(たとえば、wickedweatherは「wickedwe at her」として解析されるなど、一部の文字列は複数の可能な単語セットに分割できます)。単語のセットが1つだけ必要な場合は、単語数が最も少ないソリューションを選択するか、最小の単語長を設定することによって、最適なセットを選択するためのルールを決定する必要があります。

#!/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;
}
8
Robert Gamble

正規表現の仕事ではないと思っているのは正しいと思います。私は辞書のアイデアを使用してこれにアプローチします-辞書内の単語である最長のプレフィックスを探します。それを見つけたら、それを切り落とし、文字列の残りの部分でも同じようにします。

上記の方法はあいまいさの影響を受けます。たとえば、「drivereallyfast」は最初に「driver」を見つけ、次に「eallyfast」で問題が発生します。したがって、この状況に遭遇した場合は、バックトラックも行う必要があります。または、分割する文字列がそれほど多くないため、自動分割に失敗した文字列を手動で実行します。

3
Greg Hewgill

これは、識別子の分割または識別子名のトークン化として知られる問題に関連しています。 。 OPの場合、入力は通常の単語の連結であるように見えます。識別子の分割では、入力はソースコードからのクラス名、関数名、またはその他の識別子であり、問​​題はより困難です。これは古い質問であり、OPは問題を解決したか、先に進んだことを認識していますが、識別子スプリッターを探しているときに他の誰かがこの質問に遭遇した場合に備えて(私がそうだったように)、提供したいと思います- スパイラル ( "IdentifieRsのスプリッター:ライブラリ")。 Pythonで記述されていますが、識別子のファイル(1行に1つ)を読み取り、各ファイルを分割できるコマンドラインユーティリティが付属しています。

識別子の分割は一見難しいです。プログラマーは通常、名前を付けるときに略語、頭字語、Wordフラグメントを使用し、常に一貫した規則を使用するとは限りません。識別子がキャメルケースなどの規則に従っている場合でも、あいまいさが生じる可能性があります。

Spiral Roninと呼ばれる新しいアルゴリズムを含む、多数の識別子分割アルゴリズムを実装します。さまざまなヒューリスティックルール、英語の辞書、およびマイニングソースコードリポジトリから取得したトークン頻度のテーブルを使用します。 Roninは、キャメルケースやその他の命名規則を使用しない識別子を分割できます。これには、J2SEProjectTypeProfilerを[J2SEProjectTypeProfiler]。これには、リーダーがJ2SEを1つの単位として認識する必要があります。 Roninが分割できるもののいくつかの例を次に示します。

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

OPの質問の例を使用すると:

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

ご覧のとおり、完璧ではありません。 Roninにはいくつかのパラメーターがあり、それらを調整するとdriveourtrucksも分割できますが、プログラム識別子のパフォーマンスが低下するという犠牲を払うことに注意してください。

詳細については、 スパイラルのGitHubリポジトリ を参照してください。

2
mhucka

さて、問題自体は正規表現だけでは解決できません。解決策(おそらく最善ではない)は、辞書を取得し、辞書内の各作業に対してリスト内の各単語に正規表現を一致させ、成功するたびにスペースを追加することです。確かに、これはそれほど速くはありませんが、プログラミングは簡単で、手作業で行うよりも速くなります。

1
Zoe Gagnon

辞書ベースのソリューションが必要になります。発生する可能性のある単語の辞書が限られている場合、これは多少簡略化される可能性があります。そうしないと、他の単語の接頭辞を形成する単語が問題になります。

1
Mitch Wheat

Pythonを使用した簡単な解決策: wordsegment パッケージをインストールします:pip install wordsegment

$ echo thisisatest | python -m wordsegment
this is a test
0
Rabash

形態素解析に使用できるmlmorphと呼ばれるpythonパッケージリリースされたSanthoshthottingalがあります。

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
adam shamsudeen

私はこれのためにダウンモッドになるかもしれません、しかし秘書にそれをさせてください

手動で処理するよりも、辞書ソリューションに多くの時間を費やします。さらに、ソリューションに100%の信頼性があるとは限らないため、とにかく手動で注意を払う必要があります。

0
Dave Ward