web-dev-qa-db-ja.com

スペースなしのテキストを単語のリストに分割する方法は?

入力: _"tableapplechairtablecupboard..."_多くの単語

そのようなテキストを単語のリストに分割して取得するための効率的なアルゴリズムは何でしょうか:

出力: _["table", "Apple", "chair", "table", ["cupboard", ["cup", "board"]], ...]_

頭に浮かんだ最初のことは、すべての可能な単語(最初の文字から始まる)を調べて、可能な限り長い単語を見つけることです。position=Word_position+len(Word)から続行します

追伸.
考えられるすべての単語のリストがあります。
単語「cupboard」は「cup」と「board」のいずれかで、最長を選択します。
言語:python、しかし主なものはアルゴリズムそのものです。

89
Sergey

素朴なアルゴリズムは、実世界のデータに適用しても良い結果をもたらさないでしょう。これは、相対的な単語の頻度を活用して実際の単語のテキストに正確な結果を与える20行のアルゴリズムです。

(Wordの頻度を使用しない元の質問への回答が必要な場合は、「最長単語」の意味を正確に絞り込む必要があります。20文字の単語と10文字の3文字の単語の方が良いですか、または5つの10文字の単語を使用する方が良いでしょうか?正確な定義が決まったら、wordcostを定義する行を変更するだけで目的の意味を反映できます。)

アイデア

続行する最良の方法は、出力の分布をmodelにすることです。良い最初の近似は、すべての単語が独立して分布していると仮定することです。次に、すべての単語の相対的な頻度を知る必要があります。 Zipfの法則に従うと仮定することは合理的です。つまり、単語リスト内のランクがnである単語は、確率が約1 /(nlog[〜#〜 ] n [〜#〜])ここで、[〜#〜] n [〜#〜]は辞書の単語数です。

モデルを修正したら、動的プログラミングを使用してスペースの位置を推測できます。最も可能性の高い文は、個々の単語の確率の積を最大化する文であり、動的プログラミングで簡単に計算できます。確率を直接使用する代わりに、オーバーフローを回避するために、確率の逆数の対数として定義されたコストを使用します。

コード

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

で使用できます

s = 'thumbgreenappleactiveassignmentweeklymetaphor'
print(infer_spaces(s))

結果

私は、ウィキペディアの小さなサブセットから作成した this quick-and-dirty 125k-Word辞書を組み合わせて使用​​しています

前: thumbgreenappleactiveassignmentweeklymetaphor。
後:サムグリーンAppleアクティブな割り当ての毎週のメタファー。

前: HTMLから解析される人々のコメントのソフト拡張情報がありますが、たとえば、サムグリーンアップルアクティブ割り当て週に1回メタフォラップメタデータを使用して、文字列が非常に高速で抽出できるかどうかを照会します。

後: htmlから解析される人々のコメントのテキスト情報の塊がありますが、たとえば、親指の緑Appleアクティブな割り当て毎週メタファー親指の緑Appleなど、文字列にiには、Wordが妥当かどうかを照会するための大きな辞書があります。

前:暗闇に苦しんでいるランプのスカンティフレイムをシーンでガタガタと音を立てながら通りに沿って音を立てて通りを掃く激しい突風によってチェックされたときの、時折の間隔を除く暗く嵐の夜のトレント。

後:それは暗い嵐の夜でしたが、雨が激しい嵐に襲われ、時折、激しい突風によって通りが吹き荒れることがありました。屋上に沿って、暗闇と戦ったランプのわずかな炎を激しく揺り動かしました。

ご覧のとおり、基本的に問題はありません。最も重要な部分は、Wordリストが実際に遭遇するものと同様のコーパスにトレーニングされていることを確認することです。そうしないと、結果が非​​常に悪くなります。


最適化

実装は線形の時間とメモリを消費するため、かなり効率的です。さらに高速化が必要な場合は、Wordリストからサフィックスツリーを構築して、候補セットのサイズを小さくすることができます。

非常に大きな連続した文字列を処理する必要がある場合、過度のメモリ使用を避けるために文字列を分割するのが合理的です。たとえば、境界効果を回避するために、10000文字のブロックでテキストを処理し、両側に1000文字のマージンを追加できます。これにより、メモリ使用量が最小限に抑えられ、品質にはほとんど確実に影響しません。

158
Generic Human

トップアンサー の優れた成果に基づいて、簡単に使用できるようにpipパッケージを作成しました。

>>> import wordninja
>>> wordninja.split('derekanderson')
['derek', 'anderson']

インストールするには、pip install wordninjaを実行します。

唯一の違いはわずかです。これはlistではなくstrを返し、python3で動作し、Wordのリストを含み、非アルファ文字(アンダースコア、ダッシュ、等)。

Generic Humanに再び感謝します!

https://github.com/keredson/wordninja

35
keredson

再帰検索を使用したソリューションは次のとおりです。

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

利回り

['table', 'Apple', 'chair', 'table', 'cupboard']
['tab', 'leprechaun']
16
unutbu

トライデータ構造 を使用すると、可能性のある単語のリストが保持され、以下を実行するのにそれほど複雑ではありません。

  1. アドバンスポインター(連結文字列内)
  2. 対応するノードを検索してトライに保存します
  3. トライノードに子がある場合(たとえば、長い単語がある場合)、1に進みます。
  4. 到達したノードに子がない場合、最長のWord一致が発生しました。 Word(ノードに格納されているか、トライトラバーサル中に連結されている)を結果リストに追加し、トライのポインターをリセット(または参照をリセット)して、最初からやり直します
10
miku

Unutbuのソリューションは非常に近いものでしたが、コードが読みにくく、期待した結果が得られませんでした。 Generic Humanのソリューションには、Wordの頻度が必要であるという欠点があります。すべてのユースケースに適しているわけではありません。

分割統治アルゴリズム を使用した簡単なソリューションを次に示します。

  1. 単語数を最小化しようとしますfind_words('cupboard')は_['cupboard']_ではなく_['cup', 'board']_を返します(cupboardcup、およびboardが辞書にあると仮定します)
  2. 最適なソリューションはnot uniqueで、以下の実装はaソリューションを返します。 find_words('charactersin')は_['characters', 'in']_を返すか、_['character', 'sin']_を返します(以下を参照)。アルゴリズムを非常に簡単に変更して、すべての最適なソリューションを返すことができます。
  3. この実装では、ソリューションは memoized であるため、妥当な時間で実行されます。

コード:

_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
_

これには、私の3GHzマシンで約5秒かかります。

_result = find_words("thereismassesoftextinformationofpeoplescommentswhichisparsedfromhtmlbuttherearenodelimitedcharactersinthemforexamplethumbgreenappleactiveassignmentweeklymetaphorapparentlytherearethumbgreenappleetcinthestringialsohavealargedictionarytoquerywhetherthewordisreasonablesowhatsthefastestwayofextractionthxalot")
assert(result is not None)
print ' '.join(result)
_

htmlから解析される人々のコメントのテキスト情報の塊であるが、それらに限定された文字はない。たとえば、親指の緑Appleアクティブな割り当ての毎週のメタファーは、明らかに親指の緑Appleなど文字列には、Wordが合理的であるかどうかを照会する大きな辞書もありますので、最速の抽出方法がたくさんあります

9
Rems

https://stackoverflow.com/users/1515832/generic-human による答えは素晴らしいです。しかし、私がこれまで見た中でこれを実現するための最良の方法は、彼の著書「Beautiful Data」にピーター・ノーヴィグ自身が書いたものです。

彼のコードを貼り付ける前に、Norvigの方法がより正確である理由を説明します(ただし、コードの面では少し遅く、長くなります)。

1)サイズと精度の両方の点でデータが少し優れています(単純なランキングではなくワードカウントを使用します)2)さらに重要なことは、n-gramの背後にあるロジックがアプローチを本当に正確にしていることです。 。

彼が本で提供している例は、文字列「sitdown」を分割する問題です。文字列分割の非バイグラムメソッドはp( 'sit')* p( 'down')を考慮し、これがp( ​​'sitdown')より小さい場合-よくあることですが、分割されませんそれが、私たちは(たいていの場合)それが欲しいでしょう。

ただし、バイグラムモデルがある場合は、p( 'sit down')をバイグラム対p( 'sitdown')として評価でき、前者が勝ちます。基本的に、バイグラムを使用しない場合、分割する単語の確率を独立したものとして扱いますが、そうではない場合があり、いくつかの単語が次々と現れる可能性が高くなります。残念なことに、これらは多くの場合につなぎ合わされ、スプリッターを混乱させる言葉でもあります。

データへのリンクは次のとおりです(3つの個別の問題のデータであり、セグメンテーションは1つのみです。詳細については、この章をお読みください): http://norvig.com/ngrams/

ここにコードへのリンクがあります: http://norvig.com/ngrams/ngrams.py

これらのリンクはしばらくアップしていますが、とにかくここにコードのセグメンテーション部分をコピーして貼り付けます

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 

ワードリストを [〜#〜] dfa [〜#〜] (これは非常に遅くなります)にプリコンパイルすると、入力の一致にかかる時間は、文字列(実際には、文字列を繰り返し処理するよりも少し遅いだけです)。

これは、事実上、前述のトライアルゴリズムのより一般的なバージョンです。私はそれを完全に言及するだけです-現時点では、使用できるDFA実装はありません。 RE2 は機能しますが、Pythonバインディングにより、コンパイルされたDFAデータを破棄する前にDFAを許可する大きさを調整できるかどうかわかりませんNFA検索を行います。

2

JavaScriptに翻訳された承認済みの回答を次に示します(node.jsと、 https://github.com/keredson/wordninja の「wordninja_words.txt」ファイルが必要です)。

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();
}
1

文字列に含まれる単語の完全なリストがある場合:

Word_list = ["table", "Apple", "chair", "cupboard"]

リスト内包表記を使用してリストを反復処理し、Wordとその出現回数を特定します。

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


この関数は、リストの順序で単語のstring出力を返しますtable table Apple chair cupboard

0
zainsharif1

@ miku's を拡張してTrieを使用することを提案します。追加専用の Triepythonに実装するのが比較的簡単です。

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

その後、一連の単語からTrieベースの辞書を作成できます。

dictionary = {"a", "pea", "nut", "peanut", "but", "butt", "butte", "butter"}
trie_dictionary = TrieDictionary(words=dictionary)

次のようなツリーが生成されます(*は、Wordの開始または終了を示します):

* -> a*
 \\\ 
  \\\-> p -> e -> a*
   \\              \-> n -> u -> t*
    \\
     \\-> b -> u -> t*
      \\             \-> t*
       \\                 \-> e*
        \\                     \-> r*
         \
          \-> n -> u -> t*

単語の選択方法に関する発見的手法と組み合わせることで、これをソリューションに組み込むことができます。たとえば、短い単語よりも長い単語を好むことができます。

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 []

この関数は次のように使用できます。

>>> using_trie_longest_Word_heuristic("peanutbutter")
[ "peanut", "butter" ]

長い単語を検索するときにTrieの位置を維持するため、可能なソリューションごとに最大で1回trieをトラバースします(peanut2回ではなく:peapeanut)。最後の短絡は、最悪の場合に文字通り文字列を歩くことから私たちを救います。

最終結果はほんの一握りの検査です:

'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

このソリューションの利点は、特定の接頭辞を持つ長い単語が存在するかどうかを非常に迅速に知ることができることです。これにより、辞書に対してシーケンスの組み合わせを徹底的にテストする必要がなくなります。また、unsolvableの回答を他の実装に比べて比較的安価にすることができます。

このソリューションの欠点は、trieのメモリフットプリントが大きいことと、trieを事前に構築するコストです。

0
Matthew Story

ドイツ語の場合、機械学習を使用し、少数の単語の文字列に対して非常に適切に機能するCharSplitがあります。

https://github.com/dtuggener/CharSplit

0
Karl Adler

Unutbuのソリューションに基づいて、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;
}

入力:"tableapplechairtablecupboard"

出力:[table, Apple, chair, table, cupboard]

入力:"tableprechaun"

出力:[tab, leprechaun]

かなりありふれたバックトラッキングができるようです。文字列の最初から始めます。 Wordが見つかるまで右にスキャンします。次に、残りの文字列で関数を呼び出します。 Wordを認識せずに右端までスキャンすると、関数は「false」を返します。それ以外の場合、見つかったWordと再帰呼び出しによって返された単語のリストを返します。

例:「tableapple」。 「tab」、次に「leap」を検索しますが、「ple」に単語はありません。 「leapple」には他の言葉はありません。 「table」、次に「app」を検索します。 「le」はWor​​dではないので、Appleを試し、認識して、返します。

可能な限り長くするために、正しいソリューションを(返すのではなく)出力するだけで、続行します。次に、選択した基準(maxmax、minmax、averageなど)によって最適なものを選択します。

0
Patrick87