Boşluksuz metinler kelime listesine nasıl bölünür?


106

Giriş: "tableapplechairtablecupboard..." birçok kelime

Böyle bir metni kelime listesine ayırmak ve elde etmek için etkili bir algoritma ne olurdu:

Çıktı: ["table", "apple", "chair", "table", ["cupboard", ["cup", "board"]], ...]

Akla gelen ilk şey, olası tüm kelimelerin üzerinden geçip (ilk harften başlayarak) mümkün olan en uzun kelimeyi bulmaktır. position=word_position+len(word)

Not:
Olası tüm kelimelerin bir listesine sahibiz.
Kelime "dolap" "fincan" ve "tahta" olabilir, en uzun olanı seçin.
Dil: python, ancak asıl önemli olan algoritmanın kendisidir.


14
Dizenin "sekme" ve "sıçrama" kelimeleriyle başlamadığından emin misiniz?
Rob Hruska

Evet, net bir şekilde yapılamaz gibi görünüyor.
demalexx

@RobHruska, bu durumda mümkün olan en uzun süreyi seçerek yazdım.
Sergey

2
@Sergey - "Mümkün olan en uzun" kriteriniz, bunun bileşik kelimeler için olduğunu ima ediyordu. Ve bu durumda, ip "halıcı" olsaydı ne olurdu. "Halı" mı yoksa "kuş" mu?
Rob Hruska

2
Dizenizle birçok dictitonary kelime yoktur:['able', 'air', 'apple', 'boa', 'boar', 'board', 'chair', 'cup', 'cupboard', 'ha', 'hair', 'lea', 'leap', 'oar', 'tab', 'table', 'up']
reclosedev

Yanıtlar:


200

Saf bir algoritma, gerçek dünya verilerine uygulandığında iyi sonuçlar vermez. İşte gerçek kelime metin için doğru sonuçlar vermek için göreceli kelime frekansını kullanan 20 satırlık bir algoritma.

(Orijinal sorunuza kelime frekansı kullanmayan bir cevap istiyorsanız, "en uzun kelime" ile tam olarak neyin kastedildiğini hassaslaştırmanız gerekir: 20 harfli bir kelimeye ve on adet 3 harfli kelimeye sahip olmak daha mı iyidir? 10 harfli beş kelimeye sahip olmak daha iyidir? Kesin bir tanıma karar verdikten sonra wordcost, istenen anlamı yansıtmak için tanımlayan satırı değiştirmeniz yeterlidir.)

Fikir

Devam etmenin en iyi yolu , çıktının dağılımını modellemektir . İyi bir ilk yaklaşım, tüm kelimelerin bağımsız olarak dağıtıldığını varsaymaktır. O zaman sadece tüm kelimelerin göreceli sıklığını bilmeniz gerekir. Onlar rütbe ile kelime olduğunu, Zipf yasası izleyin varsaymak mantıklıdır n kelimelerin listesinde olasılığı kabaca 1 / (gelmiştir n log N ) N sözlükteki kelime sayısıdır.

Modeli düzelttikten sonra, alanların konumunu anlamak için dinamik programlamayı kullanabilirsiniz. En olası cümle, her bir kelimenin olasılığının ürününü en üst düzeye çıkaran cümledir ve bunu dinamik programlamayla hesaplamak kolaydır. Olasılığı doğrudan kullanmak yerine, taşmaları önlemek için olasılığın tersinin logaritması olarak tanımlanan bir maliyeti kullanırız.

Kod

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

ile kullanabileceğiniz

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

Sonuçlar

Ben kullanıyorum araya getirdiğim bu kirli hızlı ve-125k-kelime sözlük Wikipedia'nın küçük bir alt kümesinden.

Önce: thumbgreenappleactive assignmentweeklymetaphor.
Sonra: başparmak yeşil elma aktif ödev haftalık metafor.

Önce: bu, html'den ayrılan insanların yorumlarıyla ilgili önemli bilgiler, ancak örnek olarak küçük yeşil elma etkin atamaları içinhaftalık sınırlandırılmış karakterlerden daha azıhafta metafo, küçük yeşil elma veçıkırdamaçözlülüğünhavealargüçlü bir sözlüğünesözlüğünün ne kadar farklı bir yol olup olmadığını sorgulamak için.

Sonra: html'den ayrıştırılan insan yorumlarının metin bilgisi yığınları var ancak bunlarda sınırlandırılmış karakterler yok, örneğin başparmak yeşil elma aktif atama haftalık metafor görünüşe göre dizede başparmak yeşil elma vb. Ayrıca büyük bir sözlüğüm var kelimenin makul olup olmadığını sorgulayın, bu yüzden en hızlı çıkartma yöntemi thx çoktur.

Daha önce: bir karanlık ve fırtınaydıgece çarpışan kira çekilişleriydi.

Sonra: karanlık ve fırtınalı bir geceydi, yağmurlar ara sıra yağmur yağdı, ara sıra sokakları süpüren şiddetli bir rüzgârla kontrol edildi, çünkü Londra'da sahnemiz evin tepelerinde çıngırdıyor ve şiddetli bir şekilde çalkalanıyor. karanlığa karşı mücadele eden lambaların yetersiz alevi.

Gördüğünüz gibi aslında kusursuz. En önemli kısım, kelime listenizin gerçekte karşılaşacağınız şeye benzer bir külliyat için eğitildiğinden emin olmaktır, aksi takdirde sonuçlar çok kötü olacaktır.


Optimizasyon

Uygulama doğrusal miktarda zaman ve bellek tüketir, bu nedenle makul derecede etkilidir. Daha fazla hızlandırmaya ihtiyacınız varsa, aday kümesinin boyutunu küçültmek için sözcük listesinden bir sonek ağacı oluşturabilirsiniz.

Ardışık çok büyük bir dizeyi işlemeniz gerekiyorsa, aşırı bellek kullanımından kaçınmak için dizeyi bölmek mantıklı olacaktır. Örneğin, sınır efektlerinden kaçınmak için metni 10000 karakterlik bloklar ve her iki tarafta da 1000 karakterlik bir kenar boşluğu halinde işleyebilirsiniz. Bu, bellek kullanımını minimumda tutacak ve kalite üzerinde neredeyse kesinlikle hiçbir etkisi olmayacaktır.


1
peki ya iki satırlık metin?
leafiy

11
Bu kod beni uyuşturdu. Biraz anlamadım Günlük şeyleri anlamıyorum. Ama bu kodu bilgisayarımda test ettim. Sen bir dahisin.
Aditya Singh

1
Bu algoritmanın çalışma süresi nedir? Neden ahocorasick kullanmıyorsun?
RetroCode

8
Bu mükemmel. Onu bir pip paketine çevirdim : pypi.python.org/pypi/wordninjapip install wordninja
keredson

2
@wittrup words.txt, "comp": "" $ grep "^ comp $" words.txt comp `` öğesini içerir ve alfabetik olarak sıralanır. bu kod, azalan sıklıkta sıralandığını varsayar (bu gibi n-gram listeleri için ortaktır). düzgün sıralı bir liste kullanırsanız, dizeniz iyi çıkar: `` >>> wordninja.split ('namethecompanywherebonniewasemployedwhenwestarteddating') ['name', 'the', 'company', 'where', 'bonnie', ' ',' istihdam edildi ',' ne zaman ',' biz ',' başladık ',' buluşma '] "
keredson

50

En üstteki cevaptaki mükemmel çalışmaya dayanarak, pipkolay kullanım için bir paket oluşturdum .

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

Yüklemek için çalıştırın pip install wordninja .

Tek fark küçüktür. Bu, a listyerine a döndürür str, çalışırpython3 , kelime listesini içerir ve alfa olmayan karakterler (alt çizgi, kısa çizgi vb. Gibi) olsa bile düzgün şekilde bölünür.

Generic Human'a tekrar teşekkürler!

https://github.com/keredson/wordninja


2
Bunu yarattığınız için teşekkürler.
Mohit Bhatia

1
Teşekkür ederim! Onu bir paket yapmanı seviyorum. Altta yatan yöntem benim için pek işe yaramadı. Örneğin, "şezlonglar" "salon" ve "rs" olarak ayrıldı
Harry M

@keredson - Öncelikle çözüm için teşekkürler. İyi davranıyor. Ancak, "-" gibi özel karakterleri çıkarır. Bazen uzun bir dizede olduğu gibi uygun bölmeyi vermez - "WeatheringProperties byMaterial Trade Name Graph 2-1. Color Change, E, after Arizona, Florida, Cycolac® / PVC ile karşılaştırıldığında Geloy® Reçine Sistemleri [15] 25 20 15 ∆E 10 5 0 PVC, Beyaz PVC, Kahverengi C / G, BrownC / G. Capstock, bir profilin dış yüzeyine uygulanan yüzey tabakası olarak kullanılan malzemedir. bir Cycolac® alt tabakası üzerindeki Geloy® reçine kapsülü olağanüstü hava koşullarına dayanıklılık sağlar. [25] "
Rakesh Lamp Stack

GH'de bir sorun açabilir misin?
keredson

1
İyi iş, çaban için teşekkürler. Bana gerçekten çok zaman kazandırdı.
Jan Zeiseweis

17

Özyinelemeli aramayı kullanan çözüm:

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

verim

['table', 'apple', 'chair', 'table', 'cupboard']
['tab', 'leprechaun']

"kutunun dışında" çalışır, teşekkürler! Ayrıca, miku'nun söylediği gibi trie yapısını kullanmayı düşünüyorum, sadece tüm kelimeler için değil. Yine de teşekkürler!
Sergey

11

Olası kelimelerin listesini tutan bir üçlü veri yapısı kullanmak , aşağıdakileri yapmak çok karmaşık olmayacaktır:

  1. Gelişmiş işaretçi (birleştirilmiş dizede)
  2. Trie'deki ilgili düğümü arayın ve kaydedin
  3. Tri düğümünün çocukları varsa (örneğin daha uzun sözcükler varsa), 1'e gidin.
  4. Ulaşılan düğümün çocuğu yoksa, en uzun kelime eşleşmesi gerçekleşti; kelimeyi (düğümde depolanan veya sadece üçlü geçiş sırasında birleştirilen) sonuç listesine ekleyin, işaretleyiciyi üçlüde sıfırlayın (veya referansı sıfırlayın) ve baştan başlayın

3
Hedef dizginin tamamını tüketecekse, geriye dönmeniz ve "tableprechaun"ardından bölünmeniz gerekir "tab".
Daniel Fischer

Artı trie'den bahsettiğim için, ama geri dönüşün yapılması gerektiği konusunda Daniel'e de katılıyorum.
Sergey

@Daniel, en uzun eşleşme araması geriye dönük izleme gerektirmez, hayır. Seni böyle düşündüren nedir? Ve yukarıdaki algoritmanın nesi yanlış?
Devin Jeanpierre

1
İçin gerçeği @Devin "tableprechaun"baştan en uzun maçı olan "table"bırakarak "prechaun"sözlük kelimeleri bölünebilir olamaz. Bu nedenle, sizi "tab"bir "leprechaun".
Daniel Fischer

@Daniel, Üzgünüm, evet. Sorunu yanlış anladım. Düzeltilmiş algoritma, tüm olası ağaç konumlarını aynı anda takip etmelidir - AKA doğrusal zamanlı NFA araması. Ya da geri dönüş, elbette, ama bu en kötü durum üstel zamandır.
Devin Jeanpierre

9

Unutbu'nun çözümü oldukça yakındı ancak kodu okumayı zor buluyorum ve beklenen sonucu vermedi. Generic Human'ın çözümünün dezavantajı kelime frekanslarına ihtiyaç duymasıdır. Tüm kullanım durumları için uygun değildir.

İşte Divide and Conquer algoritması kullanan basit bir çözüm .

  1. Bu dener sözcük sayısını en aza indirmek Ör find_words('cupboard')dönecektir ['cupboard']yerine ['cup', 'board'](varsayılarak cupboard, cupve boarddictionnary içindedir)
  2. En uygun çözüm benzersiz değildir , aşağıdaki uygulama bir çözüm döndürür . find_words('charactersin')geri dönebilir ['characters', 'in']veya belki geri döner ['character', 'sin'](aşağıda görüldüğü gibi). Tüm optimal çözümleri döndürmek için algoritmayı kolayca değiştirebilirsiniz.
  3. Bu uygulamada çözümler hafızaya alınır, böylece makul bir sürede çalışır.

Kod:

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

Bu, 3GHz makinemde yaklaşık 5 saniye sürecektir:

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

html'den ayrıştırılmış, ancak sınırlandırılmış karakter yok, örneğin başparmak yeşil elma aktif atama haftalık metafor, dizede başparmak yeşil elma vb. bu kelime mantıklı, bu yüzden en hızlı ekstraksiyon yöntemi ne?


Bir metnin tek harfli bir kelimeyle bitemeyeceğine inanmak için hiçbir sebep yok. Bir bölmeyi daha düşünmelisiniz.
panda-34

7

Cevabı https://stackoverflow.com/users/1515832/generic-human harika. Ancak bunun şimdiye kadar gördüğüm en iyi uygulaması Peter Norvig'in "Beautiful Data" adlı kitabında yazılmıştır.

Kodunu yapıştırmadan önce, Norvig yönteminin neden daha doğru olduğunu açıklayayım (kod açısından biraz daha yavaş ve daha uzun olmasına rağmen).

1) Veriler biraz daha iyidir - hem boyut hem de hassasiyet açısından (basit bir sıralama yerine bir kelime sayısı kullanır) 2) Daha da önemlisi, yaklaşımı gerçekten bu kadar doğru yapan n-gramların arkasındaki mantıktır. .

Kitabında sunduğu örnek, bir dizeyi 'oturuş' olarak bölme sorunudur. Şimdi, bigram olmayan bir dize bölme yöntemi p ('otur') * p ('aşağı') olarak değerlendirilir ve bu p ('sitdown') değerinden daha az ise - ki bu oldukça sık görülür - bölünmez , ama biz onu isterdik (çoğu zaman).

Ancak, bigram modeline sahip olduğunuzda, p'yi ('oturun') bir bigram'a karşı p ('sitdown') olarak değerlendirebilirsiniz ve eski kazanır. Temel olarak, bigram kullanmazsanız, böldüğünüz kelimelerin olasılığını bağımsız olarak ele alır, bu durum böyle değildir, bazı kelimelerin birbiri ardına görünme olasılığı daha yüksektir. Ne yazık ki bunlar aynı zamanda çoğu durumda sık sık birbirine yapışan ve ayırıcıyı karıştıran kelimelerdir.

Verilere bağlantı burada (3 ayrı probleme ait veriler ve segmentasyon yalnızca bir tanesidir. Ayrıntılar için lütfen bölümü okuyun): http://norvig.com/ngrams/

ve işte kodun bağlantısı: http://norvig.com/ngrams/ngrams.py

Bu bağlantılar bir süredir arttı, ancak yine de kodun bölümleme kısmını kopyalayıp buraya yapıştıracağım

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 

Bu iyi çalışıyor, ama benim bütün veri kümesi bu uygulamaya çalıştığınızda, bu deyip duruyorRuntimeError: maximum recursion depth exceeded in cmp
Harry M

ngramlar kesinlikle size katlanarak daha büyük frekans diktesi, bellek ve hesaplama kullanımı ile doğruluk artışı sağlayacaktır. btw not işlevi, orada bir elek gibi bellek sızdırıyor. aramalar arasında temizlemesi gerekir.
keredson

3

Kabul edilen cevap JavaScript'e çevrilmiştir (node.js ve https://github.com/keredson/wordninja adresinden "wordninja_words.txt" dosyası gerektirir ):

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

Kelime listesini bir DFA'da önceden derlerseniz (ki bu çok yavaş olacaktır), bir girdiyi eşleştirmek için geçen süre dizenin uzunluğuyla orantılı olacaktır (aslında, dizeyi yinelemekten yalnızca biraz daha yavaş).

Bu, daha önce bahsedilen trie algoritmasının daha genel bir versiyonudur. Bunu sadece tamamlayıcı olmadığı için söylüyorum - şimdilik sadece kullanabileceğiniz bir DFA uygulaması yok. RE2 işe yarar, ancak Python bağlamalarının, derlenmiş DFA verilerini atıp NFA araması yapmadan önce bir DFA'nın ne kadar büyük olmasına izin verdiğinizi ayarlamanıza izin verip vermediğini bilmiyorum.


özellikle re2 için artı, daha önce kullanmamıştı
Sergey

0

Oldukça sıradan bir geri dönüş yapacak gibi görünüyor. İpin başında başlayın. Bir kelime bulana kadar doğru tarayın. Ardından, dizenin geri kalanında işlevi çağırın. İşlev, bir sözcüğü tanımadan en sağa doğru tararsa "yanlış" döndürür. Aksi takdirde, bulduğu kelimeyi ve özyinelemeli çağrı tarafından döndürülen kelimelerin listesini döndürür.

Örnek: "tableapple". "Sekme" yi ve ardından "sıçrayışı" bulur, ancak "ple" de kelime yoktur. "Leapple" da başka kelime yok. "Tablo" yu ve ardından "uygulamayı" bulur. "le" bir kelime değil, bu yüzden elmayı dener, tanır, geri döner.

Mümkün olan en uzun süreyi elde etmek için, yalnızca doğru çözümleri (geri döndürmek yerine) yaymaya devam edin; ardından, seçtiğiniz herhangi bir kritere göre en uygun olanı seçin (maxmax, minmax, ortalama, vb.)


İyi bir algoritma, bunun hakkında düşünüyordum. unutbu kodu bile yazdı.
Sergey

@Sergey, geriye dönük arama, üstel zaman algoritmasıdır. Bunun "iyi" yanı nedir?
Devin Jeanpierre

1
Çok basit, hızlı olduğunu söylemedi
Sergey

0

Unutbu'nun çözümüne dayanarak bir Java sürümü uyguladım:

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;
}

Giriş: "tableapplechairtablecupboard"

Çıktı: [table, apple, chair, table, cupboard]

Giriş: "tableprechaun"

Çıktı: [tab, leprechaun]



0

@ Miku'nun a kullanma önerisini genişletmek, Trieyalnızca Trieeki aşağıdakilere uygulamak nispeten basittir 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

Daha sonra Triebir dizi kelimeden temelli bir sözlük oluşturabiliriz:

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

Şuna benzer bir ağaç üretecek *(bir kelimenin başlangıcını veya sonunu gösterir):

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

Bunu, kelimelerin nasıl seçileceğine dair bir buluşsal yöntemle birleştirerek bir çözüme dahil edebiliriz. Örneğin, daha kısa kelimeleri daha uzun kelimeleri tercih edebiliriz:

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

Bu işlevi şu şekilde kullanabiliriz:

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

Daha Trieuzun ve daha uzun sözcükleri ararken konumumuzu koruduğumuz için , trieolası çözüm başına en fazla bir 2kez ( peanut: pea, için zamanlar yerine) geçiyoruz peanut. Son kısa devre, bizi en kötü durumda telin içinde hızlıca yürümekten kurtarır.

Nihai sonuç yalnızca bir avuç denetimdir:

'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

Bu çözümün bir yararı, belirli bir önek ile daha uzun kelimelerin mevcut olup olmadığını çok hızlı bir şekilde bilmenizdir; bu, sıra kombinasyonlarını bir sözlüğe karşı kapsamlı bir şekilde test etme ihtiyacını ortadan kaldırır. Aynı zamanda birunsolvable yanıta ulaşmayı diğer uygulamalara göre nispeten ucuz .

Bu çözümün dezavantajları, önden kurulum için büyük bir bellek ayak izi trieve maliyetidir trie.


0

Dizede yer alan kelimelerin kapsamlı bir listesine sahipseniz:

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

Kelimeyi ve kaç kez göründüğünü bulmak için listeyi yinelemek için liste anlamayı kullanma.

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

İşlev string, liste sırasına göre kelimelerin çıktısını döndürürtable table apple chair cupboard


0

Https://github.com/keredson/wordninja/ adresindeki yardım için çok teşekkürler

Benim açımdan Java'da da küçük bir katkı.

Genel yöntem splitContiguousWords, aynı dizinde ninja_words.txt'ye sahip (veya kodlayıcının seçimine göre değiştirilebilir) sınıftaki diğer 2 yöntemle gömülebilir. Ve yöntem splitContiguousWordsbu amaç için kullanılabilir.

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;
}

ya kelime listemiz yoksa?
shirazy

Sorguyu doğru anladıysam: Dolayısıyla yukarıdaki yaklaşımda, publicyöntem String, normal ifadeyle birinci seviyeye göre bölünmüş bir tür cümlesini kabul eder . Ve listesi ninja_wordsiçin git deposundan indirilebilir.
Arnab Das

0

Bu yardımcı olacak

from wordsegment import load, segment
load()
segment('providesfortheresponsibilitiesofperson')


-1

Kelime dağarcığınızı belirlemeniz gerekir - belki de herhangi bir ücretsiz kelime listesi işe yarar.

Bittiğinde, bir sonek ağacı oluşturmak için bu kelimeleri kullanın ve giriş akışınızı bununla eşleştirin: http://en.wikipedia.org/wiki/Suffix_tree


Bu pratikte nasıl çalışır? Sonek ağacını oluşturduktan sonra, neyi eşleştireceğinizi nasıl bileceksiniz?
John Kurlak

@JohnKurlak Diğer deterministik sonlu otomatlarda olduğu gibi - tam bir kelimenin sonu kabul etme durumudur.
Marcin

Bu yaklaşım geriye dönük izleme gerektirmiyor mu? Cevabınızda geriye
dönüşten

Neden olmasın? Aşağıda belirtildiği gibi "masa cinine" sahipseniz ne olur? Yapabileceği en uzun kelime olan "masa" ile eşleşecek ve sonra başka bir kelime bulamayacaktır. "Sekmeye" geri dönmesi ve ardından "cüce" ​​ile eşleşmesi gerekecektir.
John Kurlak

@JohnKurlak Birden fazla "şube" aynı anda canlı olabilir. Aslında, olası bir kelime başlangıcı olan her harf için ağacın içine bir jetonu itersiniz ve aynı harf diğer canlı jetonları ilerletebilir.
Marcin
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.