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


106

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

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

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

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

PS
私たちはすべての可能な単語のリストを持っています。
「cupboard」という単語は「cup」と「board」にすることができ、最も長いものを選択します。
言語:Python、ただし主要なものはアルゴリズム自体です。


14
文字列が "tab"と "leap"という単語で始まっていないと思いますか?
Rob Hruska、2012年

はい、それは明確な方法で行うことができないようです。
demalexx 2012年

@RobHruska、その場合私が書いた、可能な限り長いものを選択しました。
セルゲイ2012年

2
@Sergey-あなたの「最長」の基準は、それが複合語のためであることを意味しました。その場合、文字列が「carpetrel」の場合はどうなるでしょうか。それは「カーペット」でしょうか、それとも「ペトレル」でしょうか?
Rob Hruska、2012年

2
文字列には多くの辞書の単語があります:['able', 'air', 'apple', 'boa', 'boar', 'board', 'chair', 'cup', 'cupboard', 'ha', 'hair', 'lea', 'leap', 'oar', 'tab', 'table', 'up']
reclosedev

回答:


200

単純なアルゴリズムを実際のデータに適用すると、良い結果が得られません。これは、実際の単語のテキストに対して正確な結果を与えるために相対単語の頻度を利用する20行のアルゴリズムです。

(単語の出現頻度を使用しない元の質問への回答が必要な場合は、「最長の単語」が正確に何を意味するかを調整する必要があります。 5文字の10文字の単語を使用する方が良いですか?正確な定義に落ち着いたらwordcost、意図する意味を反映するように行の定義を変更するだけです。)

アイデア

続行する最良の方法は、出力の分布をモデル化することです。最初の近似としては、すべての単語が独立して分布していると想定します。次に、すべての単語の相対的な頻度を知る必要があります。それらがZipfの法則に従うと仮定することは合理的です。つまり、単語リストのランクnの単語は、確率1 /(n log 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))

結果

私は、ウィキペディアの小さなサブセットから集めた、この汚い125k語の辞書を使用しています。

以前: thumbgreenappleactiveassignmentweeklymetaphor。
後:青リンゴのアクティブな割り当ての毎週のメタファーを親指します。

前: htmlから解析された人々のコメントのソフト拡張情報が存在し抽出された文字列の中で、例として、緑色のリンゴのアクティブな割り当てが毎週、メタにラップされています。

後: htmlから解析される人々のコメントのテキスト情報の大部分がありますが、区切り文字はありません。たとえば、サムグリーンアップルアクティブな割り当て毎週のメタファーサムグリーンアップルなどが文字列にあるようです。単語が妥当かどうかを問い合わせるので、何が最も速い抽出方法なのでしょうか。

前:それは暗闇の中で苦しんでいたランプの微妙な炎を激しく揺さぶって、家の頂上を通り過ぎる風に木々が吹き荒れるような風に吹かれて暴風の強風によってチェックされたとき、暗く、嵐の夜、雨が降りました

後:それは暗く嵐の夜でした。雨が急流に落ちたときを除いて、時折、暴風が通りを吹き払ったことを確認しました。ロンドンにあるので、私たちのシーンは、家のてっぺんに沿ってガタガタと激しく揺れ動いています。暗闇と戦ったランプのかすかな炎。

ご覧のとおり、それは本質的に完璧です。最も重要な部分は、単語リストが実際に遭遇するものと同様のコーパスにトレーニングされていることを確認することです。そうでない場合、結果は非常に悪くなります。


最適化

実装は線形の時間とメモリを消費するため、かなり効率的です。さらに高速化が必要な場合は、単語リストから接尾辞ツリーを作成して、候補セットのサイズを減らすことができます。

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


1
2行のテキストはどうですか?
葉っぱ2014

11
このコードは私を麻痺させました。少しわかりませんでした。丸太がわかりません。しかし、私は自分のコンピューターでこのコードをテストしました。あなたは天才です。
Aditya Singh

1
このアルゴリズムの実行時間はどれくらいですか?アホコラシックを使ってみませんか?
RetroCode

8
これは素晴らしいです。私はそれをpipパッケージに変えました:pypi.python.org/pypi/wordninja pip install wordninja
keredson

2
@wittrup your words.txtcontains "comp": `` `$ grep" ^ comp $ "words.txt comp` ``そしてそれはアルファベット順にソートされます。このコードは、出現頻度の降順にソートされていることを前提としています(これは、このようなn-gramリストでは一般的です)。適切にソートされたリストを使用すると、文字列はうまく出てきます: `` `>>> wordninja.split( 'namethecompanywherebonniewasemployedwhenwestarteddating')['name'、 'the'、 'company'、 'where'、 'bonnie'、 ' was」、「employed」、「when」、「we」、「started」、「dating」] `` `
keredson

50

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

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

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

違いはわずかです。これはでlistなくを返しstr、で機能しpython3ます。単語リストが含まれ、非アルファ文字(アンダースコア、ダッシュなど)がある場合でも適切に分割されます。

Generic Humanに再度感謝します。

https://github.com/keredson/wordninja


2
これを作成してくれてありがとう。
Mohit Bhatia 2017年

1
ありがとうございました!私はあなたがそれをパッケージにしたことが大好きです。根本的な方法は私にはあまりうまくいきませんでした。たとえば、「ラウンジャー」は「ラウンジ」と「rs」に分割されました
ハリーM

@keredson-まず、解決策をありがとう。うまく動作します。ただし、「-」などの特殊文字は削除されます。長い文字列を使用するなど、適切な分割が行われない場合があります-「WeatheringPropertiesbyMaterial商品名グラフ2-1。色の変化、E、アリゾナ州、フロリダ州、Cycolac®/ Geloy®樹脂システムとPVCの比較。[15] 25 20 15 ∆E 10 5 0 PVC、白色PVC、ブラウンC / G、ブラウンC / G。キャップストックは、プロファイルの外面に適用される表面層として使用される材料です。押し出し。Cycolac®基板上のGeloy®樹脂キャップストックは、優れた耐候性を提供します。[25] "
Rakeshランプスタック

GHで問題を開くことができますか?
keredson

1
すばらしい仕事、努力をありがとう。本当に時間を節約できました。
Jan Zeiseweis、

17

これは再帰検索を使用した解決策です:

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

「すぐに使える」作品、ありがとう!ミクが言ったように、すべての単語の集合だけでなく、トライ構造も使用すると思います。とにかくありがとう!
セルゲイ2012年

11

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

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

3
ターゲットが文字列全体を消費する場合は、バックトラックする必要があり、"tableprechaun"その後で分割する必要があります"tab"
ダニエルフィッシャー

加えて、トライについて言及したことについては、ダニエルにも同意します。バックトラックを実行する必要があることにも同意します。
セルゲイ2012年

@Daniel、最長一致検索はバックトラックを必要としません、いいえ。何があなたをそう思わせたのですか?そして、上記のアルゴリズムの何が問題になっていますか?
Devin Jeanpierre 2012年

1
@Devin最初"tableprechaun"から最長の一致については"table""prechaun"であり、辞書の単語に分割できないことです。ですから、短いマッチを取る"tab"必要があり"leprechaun"ます。
Daniel Fischer

@ダニエル、申し訳ありませんが、はい。私は問題を誤解しました。修正されたアルゴリズムは、可能なすべてのツリー位置を一度に追跡する必要があります-別名線形時間NFA検索。それともバックトラック、確かに、それは最悪の場合の指数関数的な時間です。
Devin Jeanpierre 2012年

9

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

以下は、分割統治アルゴリズムを使用した簡単なソリューションです

  1. それはしようとする単語の数を最小限 Egがfind_words('cupboard')返され['cupboard']なく['cup', 'board']ことを仮定し(cupboardcup及びboard dictionnaryである)を
  2. 最適なソリューションは一意ではありません。以下の実装ソリューションを返します。find_words('charactersin')戻る['characters', 'in']かもしれないし、多分それは戻るだろう['character', 'sin'](以下を参照)。アルゴリズムを簡単に変更して、すべての最適解を返すことができます。
  3. この実装では、ソリューションは適切な時間で実行されるようにメモ化されています。

コード:

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から解析された人々のコメントのテキスト情報のreisの塊ですが、区切り文字はありません。たとえば、親指の緑のリンゴのアクティブな割り当ての毎週のメタファーは、文字列に親指の緑のリンゴなどがあるようです。言葉は合理的ですので、抽出の最も速い方法は何ですか


テキストが1文字の単語で終わらないと信じる理由はありません。もう1つ分割することを検討してください。
panda-34

7

https://stackoverflow.com/users/1515832/generic-humanによる回答は素晴らしいです。しかし、これまでに見た中で最も優れた実装は、Peter Norvig自身の著書「Beautiful Data」に書かれています。

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

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

彼の本で彼が提供する例は、文字列「シットダウン」を分割する問題です。文字列分割の非バイグラムメソッドでは、p( 'sit')* p( 'down')が考慮されます。これがp( ​​'sitdown')よりも小さい場合、これはかなり頻繁に発生します-分割されませんそれが、私たちはそれを(たいていの場合)したいと思います。

ただし、バイグラムモデルがある場合、p( 'sitdown')をバイグラムと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 

これはうまく機能しますが、これを私のデータセット全体に適用しようとすると、次のように言われますRuntimeError: maximum recursion depth exceeded in cmp
Harry M

ngramは間違いなく、周波数ディクショナリー、メモリ、および計算の使用量が指数関数的に大きくなるため、精度が向上します。ところで、メモ機能はふるいのようにメモリをリークしています。呼び出し間でクリアする必要があります。
keredson 2018

3

これは、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();
}

2

ワードリストをDFAにプリコンパイルする場合(これは非常に遅くなります)、入力の照合にかかる時間は文字列の長さに比例します(実際には、単に文字列を反復するよりも少しだけ遅くなります)。

これは、前述のトライアルゴリズムのより一般的なバージョンです。私はそれを完全に説明していません-現在のところ、使用できるDFA実装はありません。RE2は機能しますが、Pythonバインディングで、DFAがコンパイル済みのDFAデータを破棄してNFA検索を実行する前に、DFAのサイズを調整できるかどうかはわかりません。


特にre2の場合はプラス、以前は使用していませんでした
Sergey

0

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

例:「テーブルアップル」。「タブ」を検索し、次に「うるう」を検索しますが、「ple」の単語は検索しません。「leapple」の他の単語はありません。「テーブル」、「アプリ」の順に検索します。「le」は単語ではないので、appleを試して、認識して、戻ります。

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


良いアルゴリズム、それについて考えていました。unutbuはコードも作成しました。
セルゲイ2012年

@Sergey、バックトラッキング検索は指数時間アルゴリズムです。それについて「良い」とは何ですか?
Devin Jeanpierre 2012年

1
それは単純だ、速いとは言わなかった
セルゲイ

0

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]



0

@mikuの提案を拡張してを使用するTrieと、追加のみTrieを比較的簡単に実装できますpython

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)

これは次のようなツリーを生成します(*単語の最初または最後を示します):

* -> 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長い単語を検索するときにの位置を維持するため、trie可能な解決策ごとに(:、)の2時間ではなく、最大で1回トラバースします。最後の短絡により、最悪の場合の文字列を文字通りに歩く必要がなくなります。peanutpeapeanut

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

'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

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

word_list = ["table", "apple", "chair", "cupboard"]

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

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

https://github.com/keredson/wordninja/のヘルプに感謝します

私の側からJavaで同じの小さな貢献。

publicメソッドsplitContiguousWordsは、同じディレクトリにninja_words.txtを持つクラス内の他の2つのメソッドに埋め込むことができます(またはコーダーの選択に従って変更されます)。そして、この方法splitContiguousWordsは目的に使用できます。

public List<String> splitContiguousWords(String sentence) {

    String splitRegex = "[^a-zA-Z0-9']+";
    Map<String, Number> wordCost = new HashMap<>();
    List<String> dictionaryWords = IOUtils.linesFromFile("ninja_words.txt", StandardCharsets.UTF_8.name());
    double naturalLogDictionaryWordsCount = Math.log(dictionaryWords.size());
    long wordIdx = 0;
    for (String word : dictionaryWords) {
        wordCost.put(word, Math.log(++wordIdx * naturalLogDictionaryWordsCount));
    }
    int maxWordLength = Collections.max(dictionaryWords, Comparator.comparing(String::length)).length();
    List<String> splitWords = new ArrayList<>();
    for (String partSentence : sentence.split(splitRegex)) {
        splitWords.add(split(partSentence, wordCost, maxWordLength));
    }
    log.info("Split word for the sentence: {}", splitWords);
    return splitWords;
}

private String split(String partSentence, Map<String, Number> wordCost, int maxWordLength) {
    List<Pair<Number, Number>> cost = new ArrayList<>();
    cost.add(new Pair<>(Integer.valueOf(0), Integer.valueOf(0)));
    for (int index = 1; index < partSentence.length() + 1; index++) {
        cost.add(bestMatch(partSentence, cost, index, wordCost, maxWordLength));
    }
    int idx = partSentence.length();
    List<String> output = new ArrayList<>();
    while (idx > 0) {
        Pair<Number, Number> candidate = bestMatch(partSentence, cost, idx, wordCost, maxWordLength);
        Number candidateCost = candidate.getKey();
        Number candidateIndexValue = candidate.getValue();
        if (candidateCost.doubleValue() != cost.get(idx).getKey().doubleValue()) {
            throw new RuntimeException("Candidate cost unmatched; This should not be the case!");
        }
        boolean newToken = true;
        String token = partSentence.substring(idx - candidateIndexValue.intValue(), idx);
        if (token != "\'" && output.size() > 0) {
            String lastWord = output.get(output.size() - 1);
            if (lastWord.equalsIgnoreCase("\'s") ||
                    (Character.isDigit(partSentence.charAt(idx - 1)) && Character.isDigit(lastWord.charAt(0)))) {
                output.set(output.size() - 1, token + lastWord);
                newToken = false;
            }
        }
        if (newToken) {
            output.add(token);
        }
        idx -= candidateIndexValue.intValue();
    }
    return String.join(" ", Lists.reverse(output));
}


private Pair<Number, Number> bestMatch(String partSentence, List<Pair<Number, Number>> cost, int index,
                      Map<String, Number> wordCost, int maxWordLength) {
    List<Pair<Number, Number>> candidates = Lists.reverse(cost.subList(Math.max(0, index - maxWordLength), index));
    int enumerateIdx = 0;
    Pair<Number, Number> minPair = new Pair<>(Integer.MAX_VALUE, Integer.valueOf(enumerateIdx));
    for (Pair<Number, Number> pair : candidates) {
        ++enumerateIdx;
        String subsequence = partSentence.substring(index - enumerateIdx, index).toLowerCase();
        Number minCost = Integer.MAX_VALUE;
        if (wordCost.containsKey(subsequence)) {
            minCost = pair.getKey().doubleValue() + wordCost.get(subsequence).doubleValue();
        }
        if (minCost.doubleValue() < minPair.getKey().doubleValue()) {
            minPair = new Pair<>(minCost.doubleValue(), enumerateIdx);
        }
    }
    return minPair;
}

単語のリストがない場合はどうなりますか?
狂気の

クエリを正しく理解している場合:したがって、上記のアプローチでは、publicメソッドはString正規表現を使用して第1レベルに基づいて分割されたタイプの文を受け入れます。そして、そのリストはninja_wordsgit repoからダウンロードできます。
Arnab Das


-1

あなたはあなたの語彙を特定する必要があります-おそらくどんなフリーワードリストでも十分です。

完了したら、その語彙を使用してサフィックスツリーを構築し、入力ストリームとそれを照合します。http//en.wikipedia.org/wiki/Suffix_tree


これは実際にはどのように機能しますか?接尾辞ツリーを作成した後、何を一致させるかをどのようにして知ることができますか?
John Kurlak

@JohnKurlak他の決定論的有限オートマトンと同様に、完全な単語の終わりは受け入れ状態です。
Marcin

そのアプローチはバックトラックを必要としませんか?回答でバックトラックについて言及しなかった...
John Kurlak

何故なの?下記のように「テーブルプレコーン」を持っている場合はどうなりますか?可能な限り長い単語「テーブル」に一致し、別の単語は検索されません。「タブ」に戻り、「レプラコーン」と一致する必要があります。
John Kurlak

@JohnKurlak複数の「ブランチ」を同時にライブにすることができます。実際には、単語の開始の可能性があるすべての文字について、トークンをツリーにプッシュします。同じ文字が他のライブトークンを進める場合があります。
Marcin
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.