공백이없는 텍스트를 단어 목록으로 분할하는 방법은 무엇입니까?


106

입력 : "tableapplechairtablecupboard..." 많은 단어

이러한 텍스트를 단어 목록으로 분할하고 다음을 얻는 효율적인 알고리즘은 무엇입니까?

산출: ["table", "apple", "chair", "table", ["cupboard", ["cup", "board"]], ...]

가장 먼저 떠오르는 것은 가능한 모든 단어 (첫 글자로 시작)를 살펴보고 가능한 가장 긴 단어를 찾고 position=word_position+len(word)

추신 :
가능한 모든 단어 목록이 있습니다.
단어 "찬장"은 "컵"과 "보드"가 될 수 있으며 가장 긴 것을 선택하십시오.
언어 : 파이썬,하지만 중요한 것은 알고리즘 자체입니다.


14
문자열이 "tab"및 "leap"단어로 시작하지 않는 것이 확실합니까?
Rob Hruska

예, 모호하지 않은 방식으로 할 수없는 것 같습니다.
demalexx

@RobHruska, 그 경우 내가 쓴 가장 긴 선택.
Sergey

2
@Sergey- "가능한 가장 긴"기준은 복합어에 대한 것이라는 것을 의미합니다. 그리고이 경우 문자열이 "carpetrel"이면 어떻게 될까요? "카펫"일까요, 아니면 "석유"일까요?
Rob Hruska

2
당신의 문자열에 많은 dictitonary 단어가있다 :['able', 'air', 'apple', 'boa', 'boar', 'board', 'chair', 'cup', 'cupboard', 'ha', 'hair', 'lea', 'leap', 'oar', 'tab', 'table', 'up']
reclosedev

답변:


200

순진한 알고리즘은 실제 데이터에 적용될 때 좋은 결과를 제공하지 않습니다. 다음은 실제 단어 텍스트에 대한 정확한 결과를 제공하기 위해 상대 단어 빈도를 이용하는 20 줄 알고리즘입니다.

(단어 빈도를 사용하지 않는 원래 질문에 대한 답을 원하면 "가장 긴 단어"가 정확히 무엇을 의미하는지 수정해야합니다. 20 자 단어와 10 자 3 자 단어를 갖는 것이 더 낫습니까? 10 자 단어 5 개가 더 좋은가요? 정확한 정의를 정했다면 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))

결과

나는 Wikipedia의 작은 부분 집합에서 모아 놓은이 빠르고 더러운 125k 단어 사전 을 사용하고 있습니다.

이전 : thumbgreenappleactiveassignmentweeklymetaphor.
이후 : 엄지 녹색 사과 활성 할당 주간 은유.

이전 : html에서 파싱 된 사람들의 의견에 대한 소프트 확장 정보가 있습니다 . 그러나 예를 들어 엄지 그린 애플 활성 할당 주간 메타포 rapparentlytherearethumbgreenappleetcinthestringialsohavea 큰 사전은 단어가 합리적 일 정도로 가장 빠른 추출 방법인지 여부를 쿼리합니다.

이후 : html에서 파싱 된 사람들의 댓글에 대한 텍스트 정보가 많지만 그 안에 구분 된 문자가 없습니다. 단어가 합리적인지 쿼리하여 가장 빠른 추출 방법은 무엇입니까?

전에: 어둡고 폭풍이 몰아 치는 밤하늘을 타는 급류는 격렬한 바람에 의해 거리를 휩쓸었던 런던의 거리를 휩쓸었던 간헐적 인 간격을 제외하고는 우리의 풍경이 집 꼭대기를 따라 울부 짖으며 어두움에 맞서 투쟁하는 등불을 휘젓고있었습니다.

그 후 : 어둡고 폭풍우가 치는 밤이었고, 거리를 휩쓸었던 격렬한 돌풍에 의해 가끔씩 비가 내리는 것을 제외하고는 우리 장면이 집 꼭대기를 따라 덜거덕 거리고 격렬하게 선동하는 런던에 있기 때문입니다. 어둠에 맞서 싸운 램프의 희미한 불꽃.

보시다시피 본질적으로 완벽합니다. 가장 중요한 부분은 단어 목록이 실제로 만나게 될 것과 유사한 말뭉치로 훈련되었는지 확인하는 것입니다. 그렇지 않으면 결과가 매우 나빠질 것입니다.


최적화

구현은 선형적인 시간과 메모리를 사용하므로 상당히 효율적입니다. 추가 속도 향상이 필요한 경우 단어 목록에서 접미사 트리를 만들어 후보자 집합의 크기를 줄일 수 있습니다.

매우 큰 연속 문자열을 처리해야하는 경우 과도한 메모리 사용을 방지하기 위해 문자열을 분할하는 것이 합리적입니다. 예를 들어 경계 효과를 피하기 위해 텍스트를 10000 자 블록과 양쪽에 1000 자 여백으로 처리 할 수 ​​있습니다. 이것은 메모리 사용량을 최소로 유지하고 품질에 거의 영향을 미치지 않습니다.


1
두 줄 텍스트는 어떻습니까?
leafiy 2014

11
이 코드는 나를 무감각하게 만들었다. 나는 조금 이해하지 못했습니다. 로그 내용을 이해하지 못합니다. 하지만 내 컴퓨터에서이 코드를 테스트했습니다. 너는 천재 야.
Aditya Singh

1
이 알고리즘의 실행 시간은 얼마입니까? 왜 ahocorasick을 사용하지 않습니까?
RetroCode

8
이것은 훌륭합니다. 나는 픽처 인 픽처 패키지로를 설정했습니다 pypi.python.org/pypi/wordninja pip install wordninja
keredson

2
@wittrup words.txt에는 "comp":```$ grep "^ comp $"words.txt comp```가 포함되어 있으며 알파벳순으로 정렬됩니다. 이 코드는 출현 빈도를 줄이면서 정렬되었다고 가정합니다 (이와 같은 n-gram 목록에서 일반적 임). 적절하게 정렬 된 목록을 사용하면 문자열이 잘 나온다 :```>>> wordninja.split ( 'namethecompanywherebonniewasemployedwhenwestarteddating') [ 'name', 'the', 'company', 'where', 'bonnie', ' ''```] '', '', '우리는', '시작했을 때', '데이트를 고용했다
keredson

50

뛰어난 작업을 바탕으로 상위 답변pip 사용하기 쉬운 패키지를 만들었습니다 .

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

설치하려면 pip install wordninja.

유일한 차이점은 사소합니다. 이것은list 가 아닌 a를str 에서 작동 python3하며 단어 목록을 포함하며 알파가 아닌 문자 (밑줄, 대시 등)가 있어도 제대로 분할됩니다.

Generic Human에게 다시 한 번 감사드립니다!

https://github.com/keredson/wordninja


2
이것을 만들어 주셔서 감사합니다.
Mohit Bhatia

1
감사합니다! 나는 당신이 그것을 패키지로 만든 것을 좋아합니다. 기본 방법은 저에게 잘 작동하지 않았습니다. 예를 들어, "의자"는 "라운지"와 "RS"로 분할되었다
해리 M

@keredson-우선 솔루션에 감사드립니다. 잘 작동합니다. 그러나 "-"등과 같은 특수 문자를 제거합니다. 때때로 긴 문자열을 사용하여 "WeatheringPropertiesbyMaterial 상표명 그래프 2-1. Color Change, E, after Arizona, Florida, Cycolac® / Geloy® 수지 시스템과 PVC 비교. [15] 25 20 15 ∆E 10 5 0 PVC, 흰색 PVC, 갈색 C / G, 갈색 C / G. 캡 스톡은 프로파일의 외부 표면에 적용되는 표면층으로 사용되는 재료입니다. 압출. Cycolac® 기판 위의 Geloy® 수지 캡 스톡은 뛰어난 내후성을 제공합니다. [25] "
Rakesh Lamp Stack

GH에서 이슈를 열 수 있습니까?
keredson

1
수고하셨습니다. 노력해 주셔서 감사합니다. 정말 많은 시간을 절약했습니다.
월 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']

"즉시"작동합니다. 감사합니다! 나는 또한 모든 단어의 집합이 아니라 miku가 말한 것처럼 trie 구조를 사용한다고 생각합니다. 어쨌든 감사합니다!
Sergey

11

가능한 단어 목록을 보유 하는 trie 데이터 구조를 사용 하면 다음을 수행하는 것이 너무 복잡하지 않습니다.

  1. 고급 포인터 (연결된 문자열)
  2. 트라이에서 해당 노드를 조회하고 저장합니다.
  3. 트라이 노드에 자식이있는 경우 (예 : 더 긴 단어가 있음) 1로 이동합니다.
  4. 도달 한 노드에 자식이 없으면 가장 긴 단어 일치가 발생합니다. 결과 목록에 단어 (노드에 저장되거나 트라이 순회 중에 연결됨)를 추가하고 트라이에서 포인터를 재설정 (또는 참조 재설정) 한 다음 다시 시작합니다.

3
대상이 전체 문자열을 소비하는 경우, 당신은 철수해야 "tableprechaun"다음 후 분리되어야한다 "tab".
Daniel Fischer

또한 트라이에 대해 언급했지만 역 추적을 수행해야한다는 Daniel의 의견에도 동의합니다.
Sergey

@Daniel, 최장 일치 검색에는 역 추적이 필요하지 않습니다. 그렇게 생각하는 이유는 무엇입니까? 그리고 위의 알고리즘에 어떤 문제가 있습니까?
Devin Jeanpierre 2012 년

1
@Devin "tableprechaun"처음부터 가장 긴 일치 항목은 "table", 이탈 이라는 사실은 "prechaun"사전 단어로 나눌 수 없습니다. 당신은 그래서 짧은 경기를 취할 "tab"당신을 떠나 "leprechaun".
Daniel Fischer

@ 다니엘, 죄송합니다. 나는 문제를 오해했다. 수정 된 알고리즘은 가능한 모든 트리 위치를 한 번에 추적해야합니다 (일명 선형 시간 NFA 검색). 그렇지 않으면 역 추적이지만 최악의 경우 지수 시간입니다.
Devin Jeanpierre

9

Unutbu의 솔루션은 매우 가까웠지만 코드를 읽기가 어려웠고 예상 한 결과를 얻지 못했습니다. Generic Human의 솔루션에는 단어 빈도가 필요하다는 단점이 있습니다. 모든 사용 사례에 적합하지는 않습니다.

다음은 Divide and Conquer 알고리즘을 사용하는 간단한 솔루션 입니다.

  1. 그것은 시도 단어의 수를 최소화 예는 find_words('cupboard')반환 ['cupboard']이 아니라 ['cup', 'board'](그 가정을 cupboard, cup그리고 board사전도에)
  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 대량이지만 구분 된 문자가 없습니다. 예를 들어 thumb green apple 활성 할당 주간 은유 분명히 문자열에 thumb green apple 등이 있는지 여부를 쿼리 할 큰 사전이 있습니다. 단어가 합리적이므로 가장 빠른 추출 방법은 무엇입니까?


텍스트가 한 글자로 끝날 수 없다고 믿을 이유가 없습니다. 하나의 분할을 더 고려해야합니다.
panda-34

7

https://stackoverflow.com/users/1515832/generic-human 의 답변 은 훌륭합니다. 그러나 내가 본 최고의 구현은 Peter Norvig가 자신의 책 'Beautiful Data'에 직접 썼습니다.

그의 코드를 붙여 넣기 전에 Norvig의 방법이 더 정확한 이유를 확장 해 보겠습니다 (코드 측면에서 약간 느리고 길지만).

1) 데이터는 크기와 정밀도 측면에서 조금 더 좋습니다 (단순한 순위보다는 단어 수를 사용합니다) .2) 더 중요한 것은 접근 방식을 매우 정확하게 만드는 것은 n-gram의 논리입니다. .

그가 책에서 제공하는 예는 'sitdown'문자열을 분할하는 문제입니다. 이제 비 그램 문자열 분할 방법은 p ( 'sit') * p ( 'down')를 고려할 것이며, 이것이 p ( 'sitdown')보다 작 으면-매우 자주 발생합니다-분할되지 않습니다 하지만 우리는 (대부분의 경우) 원합니다.

그러나 bigram 모델을 사용하면 p ( 'sit down')를 bigram 대 p ( 'sitdown')으로 평가할 수 있으며 전자가 이깁니다. 기본적으로 bigrams를 사용하지 않으면 분할하는 단어의 확률을 독립적으로 처리합니다. 그렇지 않은 경우 일부 단어가 차례로 나타날 가능성이 더 높습니다. 불행히도 그것들은 종종 많은 경우에 함께 붙어서 스플리터를 혼동하는 단어입니다.

다음은 데이터 링크입니다 (3 개의 개별 문제에 대한 데이터이며 세분화는 단 하나입니다. 자세한 내용은 해당 장을 참조하십시오). 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
해리 M

ngrams는 기하 급수적으로 더 큰 주파수 사전, 메모리 및 계산 사용으로 정확도 향상을 확실히 제공합니다. btw 메모 기능이 체처럼 메모리를 누출하고 있습니다. 통화 사이에 지워야합니다.
keredson

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 는 작동하지만 컴파일 된 DFA 데이터를 버리고 NFA 검색을 수행하기 전에 Python 바인딩을 사용하여 DFA의 크기를 조정할 수 있는지 여부를 모르겠습니다.


특히 플러스 RE2 위해, 전에 사용하지 않은
세르게이

0

상당히 평범한 역 추적이 될 것 같습니다. 문자열의 시작부터 시작하십시오. 단어가있을 때까지 바로 스캔하십시오. 그런 다음 나머지 문자열에서 함수를 호출합니다. 단어를 인식하지 않고 오른쪽 끝까지 스캔하면 함수는 "false"를 반환합니다. 그렇지 않으면 찾은 단어와 재귀 호출에서 반환 된 단어 목록을 반환합니다.

예 : "tableapple". "tab", "leap"순으로 검색되지만 "ple"에는 단어가 없습니다. "leapple"에는 다른 단어가 없습니다. "테이블"을 찾은 다음 "앱"을 찾습니다. "le"은 단어가 아니므로 사과를 시도하고 인식하고 반환합니다.

가능한 한 오래 가지려면 계속해서 올바른 솔루션을 내 보냅니다 (반환하지 않고). 그런 다음 선택한 기준 (최대, 최소, 평균 등)에 따라 최적의 항목을 선택하십시오.


좋은 알고리즘은 그것에 대해 생각하고있었습니다. unutbu도 코드를 작성했습니다.
Sergey

@Sergey, 역 추적 검색은 지수 시간 알고리즘입니다. 그것에 대해 "좋은"것은 무엇입니까?
Devin Jeanpierre

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대한 시간 peanut: pea, peanut). 마지막 단락은 최악의 경우 문자열을 통해 숯불 모양으로 걷는 것을 방지합니다.

최종 결과는 몇 가지 검사입니다.

'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에서 동일한 작은 기여.

공용 메소드 splitContiguousWords는 동일한 디렉토리에 ninja_words.txt가있는 클래스의 다른 두 메소드와 함께 임베드 될 수 있습니다 (또는 코더의 선택에 따라 수정 됨). 그리고 그 방법 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;
}

단어 목록이 없으면 어떨까요?
shirazy

쿼리를 올바르게 이해 한 경우 : 따라서 위의 접근 방식에서 public메서드는 String정규식의 첫 번째 수준을 기반으로 분할되는 유형의 문장을 허용합니다 . 목록은 ninja_wordsgit repo에서 다운로드 할 수 있습니다.
Arnab Das


-1

당신은 당신의 어휘를 식별 할 필요가 있습니다-아마도 어떤 무료 단어 목록이든 가능할 것입니다.

완료되면 해당 어휘를 사용하여 접미사 트리를 만들고 입력 스트림을 http://en.wikipedia.org/wiki/Suffix_tree 와 일치시킵니다 .


실제로 어떻게 작동할까요? 접미사 트리를 만든 후 무엇을 일치 시킬지 어떻게 알 수 있습니까?
John Kurlak

@JohnKurlak 다른 결정 론적 유한 오토 마톤과 마찬가지로 완전한 단어의 끝은 수용 상태입니다.
Marcin

이러한 접근 방식에는 역 추적이 필요하지 않습니까? 당신은 ... 당신의 대답에 되돌아 언급하지 않았다
존 Kurlak에게

왜 안돼? 아래에 언급 된대로 "tableprechaun"이 있으면 어떻게됩니까? 가능한 가장 긴 단어 인 "table"과 일치하며 다른 단어를 찾지 못합니다. "탭"으로 되돌아 간 다음 "leprechaun"과 일치해야합니다.
John Kurlak

@JohnKurlak 여러 "분기"를 동시에 사용할 수 있습니다. 실제로, 단어 시작이 가능한 모든 문자에 대해 토큰을 트리에 밀어 넣고 동일한 문자가 다른 라이브 토큰을 진행시킬 수 있습니다.
Marcin
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.