Làm thế nào để tách văn bản không có khoảng trắng thành danh sách các từ?


106

Đầu vào: "tableapplechairtablecupboard..." nhiều từ

Thuật toán hiệu quả sẽ là gì để tách văn bản đó thành danh sách các từ và nhận được:

Đầu ra: ["table", "apple", "chair", "table", ["cupboard", ["cup", "board"]], ...]

Điều đầu tiên cần lưu ý là xem qua tất cả các từ có thể (bắt đầu bằng chữ cái đầu tiên) và tìm từ dài nhất có thể, tiếp tục từ position=word_position+len(word)

PS
Chúng tôi có một danh sách tất cả các từ có thể.
Từ "tủ" có thể là "cốc" và "bảng", chọn dài nhất.
Ngôn ngữ: python, nhưng điều chính là chính thuật toán.


14
Bạn có chắc rằng chuỗi đó không bắt đầu bằng các từ "tab" và "bước nhảy"?
Rob Hruska

Có, có vẻ như nó không thể được thực hiện một cách rõ ràng.
demalexx

@RobHruska, trong trường hợp đó tôi đã viết, chọn dài nhất có thể.
Sergey

2
@Sergey - Tiêu chí "dài nhất có thể" của bạn ngụ ý rằng nó dành cho từ ghép. Và trong trường hợp đó, điều gì sẽ xảy ra nếu chuỗi là "thảm". Nó sẽ là "thảm", hay "petrel"?
Rob Hruska

2
Có nhiều từ dictitonary trong chuỗi của bạn:['able', 'air', 'apple', 'boa', 'boar', 'board', 'chair', 'cup', 'cupboard', 'ha', 'hair', 'lea', 'leap', 'oar', 'tab', 'table', 'up']
reclosedev

Câu trả lời:


200

Một thuật toán ngây thơ sẽ không cho kết quả tốt khi áp dụng cho dữ liệu trong thế giới thực. Đây là một thuật toán 20 dòng khai thác tần suất từ ​​tương đối để đưa ra kết quả chính xác cho văn bản từ thực.

(Nếu bạn muốn có câu trả lời cho câu hỏi ban đầu không sử dụng tần suất từ, bạn cần phải tinh chỉnh ý nghĩa chính xác của "từ dài nhất": tốt hơn là nên có một từ 20 chữ cái và 10 từ 3 chữ cái hay là tốt hơn nên có năm từ 10 chữ cái? Khi bạn đã xác định được định nghĩa chính xác, bạn chỉ cần thay đổi dòng xác định wordcostđể phản ánh ý nghĩa dự định.)

Ý tưởng

Cách tốt nhất để tiến hành là lập mô hình phân phối đầu ra. Một phép gần đúng đầu tiên tốt là giả sử tất cả các từ được phân phối độc lập. Sau đó, bạn chỉ cần biết tần suất tương đối của tất cả các từ. Có lý khi cho rằng chúng tuân theo định luật Zipf, tức là từ có thứ hạng n trong danh sách các từ có xác suất xấp xỉ 1 / ( n log N ) trong đó N là số từ trong từ điển.

Khi bạn đã cố định mô hình, bạn có thể sử dụng lập trình động để suy ra vị trí của các khoảng trắng. Câu có nhiều khả năng nhất là câu tối đa hóa tích xác suất của từng từ riêng lẻ và rất dễ tính toán nó bằng lập trình động. Thay vì sử dụng trực tiếp xác suất, chúng tôi sử dụng chi phí được xác định là logarit của nghịch đảo của xác suất để tránh tràn.

Mật mã

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

mà bạn có thể sử dụng với

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

Kết quả

Tôi đang sử dụng từ điển 125k từ nhanh chóng và bẩn thỉu này, tôi đã tổng hợp lại từ một tập hợp con nhỏ của Wikipedia.

Trước: thumbgreenappleactiveassignmentweeklymetaphor.
Sau: ngón tay cái màu xanh lá cây táo hoạt động nhiệm vụ ẩn dụ hàng tuần.

Trước: thereismassesoftextinformationofpe Peoplecommentswhichisparsedfromhtmlbuttherearen odelimitedcharactersinthemforexamplethumbgreenappleactiveassignmentweeklymetapho rapp Rõ ràng là có mặt tại nhà

Sau: có rất nhiều thông tin văn bản về nhận xét của mọi người được phân tích cú pháp từ html nhưng không có ký tự phân tách trong đó, ví dụ: ngón tay cái quả táo xanh hoạt động ẩn dụ hàng tuần dường như có quả táo xanh, v.v. trong chuỗi tôi cũng có một từ điển lớn để truy vấn xem từ có hợp lý không, vì vậy cách nhanh nhất để khai thác thx rất nhiều.

Trước: itwasadarkandstormynighttherainfellintorrentsexceptatoccasionalintervalswhenitwascheckedbyaviolentgustofwindwhichsweptupthestreetsforitisinlondonthatoursceneliesrattlingalongthehousetopsandfiercelyagatingthescantyflameofthelampsthatstarklednessagains.

Sau đó : đó là một đêm tối và giông bão, mưa rơi thành xối xả ngoại trừ thỉnh thoảng bị gió giật dữ dội quét qua các đường phố vì cảnh của chúng tôi nằm lăn tăn dọc theo bẫy chuột và kích động dữ dội ngọn lửa nhỏ của những ngọn đèn đấu tranh chống lại bóng tối.

Như bạn có thể thấy nó về cơ bản là hoàn mỹ. Phần quan trọng nhất là đảm bảo danh sách từ của bạn được đào tạo theo ngữ liệu tương tự như những gì bạn sẽ thực sự gặp phải, nếu không kết quả sẽ rất tệ.


Tối ưu hóa

Việc thực hiện tiêu tốn một lượng thời gian và bộ nhớ tuyến tính, vì vậy nó khá hiệu quả. Nếu bạn cần tăng tốc hơn nữa, bạn có thể xây dựng một cây hậu tố từ danh sách từ để giảm kích thước của tập hợp các ứng cử viên.

Nếu bạn cần xử lý một chuỗi liên tiếp rất lớn, sẽ là hợp lý để chia chuỗi để tránh sử dụng quá nhiều bộ nhớ. Ví dụ: bạn có thể xử lý văn bản theo khối 10000 ký tự cộng với lề 1000 ký tự ở hai bên để tránh hiệu ứng ranh giới. Điều này sẽ giữ cho việc sử dụng bộ nhớ ở mức tối thiểu và hầu như không ảnh hưởng đến chất lượng.


1
những gì về văn bản hai dòng?
leafiy

11
Mã này đã khiến tôi tê liệt. Tôi đã không hiểu một chút. Tôi không hiểu những thứ trong bản ghi. Nhưng tôi đã thử nghiệm mã này trên máy tính của mình. Bạn là một thiên tài.
Aditya Singh

1
Thời gian chạy của thuật toán này là bao nhiêu? Tại sao bạn không sử dụng ahocorasick?
RetroCode

8
Thật tuyệt vời. Tôi đã biến nó thành một gói pip: pypi.python.org/pypi/wordninja pip install wordninja
keredson

2
@wittrup của bạn words.txtchứa "comp": `` `$ grep" ^ comp $ "words.txt comp` `` và nó được sắp xếp theo thứ tự bảng chữ cái. mã này giả định rằng nó được sắp xếp theo tần suất xuất hiện giảm dần (điều này thường xảy ra đối với danh sách n-gram như thế này). nếu bạn sử dụng danh sách được sắp xếp hợp lý, chuỗi của bạn sẽ ổn: `` >>> wordninja.split ('namethecompanywherebonniewasenitionedwhenwestarteddating') ['name', 'the', 'company', 'where', 'bonnie', ' là ',' được tuyển dụng ',' khi nào ',' chúng tôi ',' bắt đầu ',' hẹn hò '] ``
keredson

50

Dựa trên thành tích xuất sắc trong câu trả lời hàng đầu , tôi đã tạo một pipgói để dễ sử dụng.

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

Để cài đặt, hãy chạy pip install wordninja.

Sự khác biệt duy nhất là nhỏ. Điều này trả về một listthay vì a str, nó hoạt động trong python3, nó bao gồm danh sách từ và phân chia đúng cách ngay cả khi có các ký tự không phải alpha (như dấu gạch dưới, dấu gạch ngang, v.v.).

Một lần nữa xin cảm ơn Generic Human!

https://github.com/keredson/wordninja


2
Cảm ơn vì đã tạo ra cái này.
Mohit Bhatia

1
Cảm ơn bạn! Tôi yêu cái bạn đã làm cho nó một gói. Phương pháp cơ bản không hoạt động tốt cho tôi. Ví dụ: "ghế dài" được chia thành "phòng chờ" và "rs"
Harry M

@keredson - Trước hết, cảm ơn vì giải pháp. Nó hoạt động tốt. Tuy nhiên, nó loại bỏ các ký tự đặc biệt như "-", v.v. Đôi khi nó không cho phép phân chia thích hợp giống như nói một chuỗi dài - "WeatheringPropertiesbyMaterial Trade Name Graph 2-1. Color Change, E, sau Arizona, Florida, Cycolac® / Hệ thống nhựa geloy® So với PVC. [15] 25 20 15 ∆E 10 5 0 PVC, PVC trắng, nâu C / G, nâuC / G. Capstock là vật liệu được sử dụng làm lớp bề mặt được áp dụng cho bề mặt bên ngoài của hồ sơ . đùn Geloy® nhựa capstock trên một chất nền Cycolac® cung cấp thời tiết nổi bật [25]".
Rakesh đèn stack

bạn có thể mở một vấn đề trong GH?
keredson

1
Công việc tốt, cảm ơn cho nỗ lực. Nó thực sự đã giúp tôi tiết kiệm rất nhiều thời gian.
Jan Zeiseweis,

17

Đây là giải pháp sử dụng tìm kiếm đệ quy:

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

hoa lợi

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

hoạt động "ra khỏi hộp", cảm ơn bạn! Tôi nghĩ cũng nên sử dụng cấu trúc trie như miku đã nói, không chỉ tập hợp tất cả các từ. Dẫu sao cũng xin cảm ơn!
Sergey

11

Sử dụng cấu trúc dữ liệu trie , chứa danh sách các từ có thể, sẽ không quá phức tạp để thực hiện những việc sau:

  1. Con trỏ nâng cao (trong chuỗi được nối)
  2. Tra cứu và lưu trữ nút tương ứng trong bộ ba
  3. Nếu nút trie có con (ví dụ: có các từ dài hơn), chuyển đến 1.
  4. Nếu nút được tiếp cận không có nút con nào, một kết quả khớp từ dài nhất đã xảy ra; thêm từ (được lưu trữ trong nút hoặc chỉ được nối trong khi truyền qua trie) vào danh sách kết quả, đặt lại con trỏ trong trie (hoặc đặt lại tham chiếu) và bắt đầu lại

3
Nếu mục tiêu là sử dụng toàn bộ chuỗi, bạn sẽ cần phải quay lại, "tableprechaun"sau đó phải được chia nhỏ sau đó "tab".
Daniel Fischer

Ngoài việc đề cập đến trie, nhưng tôi cũng đồng ý với Daniel, rằng cần phải thực hiện backtracking.
Sergey

@Daniel, tìm kiếm đối sánh dài nhất không cần theo dõi ngược lại, không. Điều gì làm cho bạn nghĩ? Và có gì sai với thuật toán trên?
Devin Jeanpierre

1
@Devin Thực tế là đối "tableprechaun"với trận đấu dài nhất từ ​​đầu là "table"rời đi "prechaun", không thể tách thành các từ điển. Vì vậy, bạn phải có trận đấu ngắn hơn "tab"để lại bạn với a "leprechaun".
Daniel Fischer

@Daniel, Xin lỗi, vâng. Tôi đã hiểu sai vấn đề. Thuật toán đã hiệu chỉnh nên theo dõi tất cả các vị trí cây có thể có cùng một lúc - Tìm kiếm NFA theo thời gian tuyến tính AKA. Hoặc ngược lại, chắc chắn, nhưng đó là thời gian theo cấp số nhân trong trường hợp xấu nhất.
Devin Jeanpierre

9

Giải pháp của Unutbu khá gần gũi nhưng tôi thấy mã khó đọc và nó không mang lại kết quả như mong đợi. Giải pháp của Generic Human có nhược điểm là nó cần tần số từ. Không thích hợp cho mọi trường hợp sử dụng.

Đây là một giải pháp đơn giản sử dụng thuật toán Chia và Chinh phục .

  1. Nó cố gắng giảm thiểu số lượng từ Eg find_words('cupboard')sẽ trả về ['cupboard']hơn là ['cup', 'board'](giả sử rằng cupboard, cupboardđang ở trong chu kỳ)
  2. Giải pháp tối ưu không phảiduy nhất , việc triển khai bên dưới trả về một giải pháp. find_words('charactersin')có thể trở lại ['characters', 'in']hoặc có thể nó sẽ trở lại ['character', 'sin'](như được thấy bên dưới). Bạn có thể dễ dàng sửa đổi thuật toán để trả về tất cả các giải pháp tối ưu.
  3. Trong cách triển khai này, các giải pháp được ghi nhớ để nó chạy trong một thời gian hợp lý.

Mật mã:

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

Quá trình này sẽ mất khoảng 5 giây trên máy 3GHz của tôi:

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

khối lượng lớn thông tin văn bản của các bình luận của mọi người được phân tích cú pháp từ html nhưng không có ký tự phân cách nào, ví dụ như ngón tay cái quả táo xanh hoạt động ẩn dụ hàng tuần dường như có quả táo xanh, v.v. trong chuỗi tôi cũng có một từ điển lớn để truy vấn xem từ đó là hợp lý vì vậy cách nhanh nhất để khai thác lô thxa là gì


Không có lý do gì để tin rằng một văn bản không thể kết thúc bằng một từ đơn. Bạn nên xem xét một lần chia nữa.
panda-34

7

Câu trả lời của https://stackoverflow.com/users/1515832/generic-human là tuyệt vời. Nhưng cách thực hiện điều này tốt nhất mà tôi từng thấy là do chính Peter Norvig viết trong cuốn sách 'Dữ liệu đẹp' của anh ấy.

Trước khi tôi dán mã của anh ấy, hãy để tôi mở rộng lý do tại sao phương pháp của Norvig chính xác hơn (mặc dù chậm hơn một chút và lâu hơn về mặt mã).

1) Dữ liệu tốt hơn một chút - cả về kích thước và độ chính xác (anh ấy sử dụng số từ thay vì xếp hạng đơn giản) 2) Quan trọng hơn, đó là logic đằng sau n-gram thực sự làm cho cách tiếp cận trở nên chính xác .

Ví dụ mà anh ấy cung cấp trong cuốn sách của mình là vấn đề tách một chuỗi 'sitdown'. Bây giờ, một phương pháp tách chuỗi không phải bigram sẽ xem xét p ('sit') * p ('down') và nếu điều này nhỏ hơn p ('sitdown') - trường hợp này thường xảy ra - nó sẽ KHÔNG chia nó, nhưng chúng tôi muốn nó (hầu hết thời gian).

Tuy nhiên, khi bạn có mô hình bigram, bạn có thể đánh giá p ('sit down') là bigram so với p ('sitdown') và chiến thắng trước đó. Về cơ bản, nếu bạn không sử dụng bigrams, nó sẽ coi xác suất các từ bạn đang tách là độc lập, điều này không đúng, một số từ có nhiều khả năng xuất hiện lần lượt hơn. Thật không may, đó cũng là những từ thường bị dính với nhau trong rất nhiều trường hợp và gây nhầm lẫn cho bộ chia.

Đây là liên kết đến dữ liệu (đó là dữ liệu cho 3 vấn đề riêng biệt và phân đoạn chỉ là một. Vui lòng đọc chương để biết chi tiết): http://norvig.com/ngrams/

và đây là liên kết đến mã: http://norvig.com/ngrams/ngrams.py

Các liên kết này đã hoạt động một thời gian, nhưng tôi vẫn sao chép, dán phần phân đoạn của mã vào đây

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 

Này hoạt động tốt, nhưng khi tôi cố gắng áp dụng điều này trên toàn bộ dữ liệu của tôi, nó giữ nóiRuntimeError: maximum recursion depth exceeded in cmp
Harry M

ngrams chắc chắn sẽ cung cấp cho bạn sự tăng cường độ chính xác với việc sử dụng tần số đọc, bộ nhớ và tính toán lớn hơn theo cấp số nhân. btw chức năng ghi nhớ đang rò rỉ bộ nhớ như một cái sàng ở đó. nên xóa nó giữa các cuộc gọi.
keredson

3

Đây là câu trả lời được chấp nhận được dịch sang JavaScript (yêu cầu node.js và tệp "wordninja_words.txt" từ https://github.com/keredson/wordninja ):

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

Nếu bạn biên dịch trước danh sách từ thành một DFA (sẽ rất chậm), thì thời gian cần để khớp một đầu vào sẽ tỷ lệ thuận với độ dài của chuỗi (trên thực tế, chỉ chậm hơn một chút so với việc chỉ lặp qua chuỗi).

Đây thực sự là một phiên bản tổng quát hơn của thuật toán trie đã được đề cập trước đó. Tôi chỉ đề cập đến nó cho danh sách hoàn chỉnh - hiện tại, không có triển khai DFA nào mà bạn có thể sử dụng. RE2 sẽ hoạt động, nhưng tôi không biết liệu các liên kết Python có cho phép bạn điều chỉnh mức độ lớn mà bạn cho phép DFA hay không trước khi nó ném dữ liệu DFA đã biên dịch và thực hiện tìm kiếm NFA.


đặc biệt là cộng cho re2, không sử dụng nó trước
Sergey

0

Có vẻ như việc bẻ khóa ngược khá trần tục sẽ làm được. Bắt đầu từ việc ăn xin của chuỗi. Quét phải cho đến khi bạn có một từ. Sau đó, gọi hàm trên phần còn lại của chuỗi. Hàm trả về "false" nếu nó quét hết về bên phải mà không nhận ra từ nào. Nếu không, trả về từ mà nó tìm thấy và danh sách các từ được trả về bởi lệnh gọi đệ quy.

Ví dụ: "tableapple". Tìm "tab", sau đó "nhảy", nhưng không có từ nào trong "ple". Không có từ nào khác trong "rò rỉ". Tìm "bảng", sau đó tìm "ứng dụng". "le" không phải là một từ, vì vậy hãy thử apple, nhận ra, quay lại.

Để đạt được hiệu quả lâu nhất có thể, hãy tiếp tục, chỉ đưa ra (thay vì quay lại) các giải pháp chính xác; sau đó, chọn cái tối ưu theo bất kỳ tiêu chí nào bạn chọn (maxmax, minmax, trung bình, v.v.)


Thuật toán tốt, đã suy nghĩ về nó. unutbu thậm chí đã viết mã.
Sergey

@Sergey, tìm kiếm theo dõi ngược là một thuật toán theo thời gian hàm mũ. "Tốt" về nó là gì?
Devin Jeanpierre

1
Nó chỉ đơn giản, không nói nó nhanh
Sergey

0

Dựa trên giải pháp của unutbu, tôi đã triển khai một phiên bản 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;
}

Đầu vào: "tableapplechairtablecupboard"

Đầu ra: [table, apple, chair, table, cupboard]

Đầu vào: "tableprechaun"

Đầu ra: [tab, leprechaun]



0

Mở rộng đề xuất của @ miku để sử dụng a Trie, append-only Trietương đối dễ triển khai trong 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

Sau đó, chúng tôi có thể xây dựng một Trietừ điển dựa trên từ một tập hợp các từ:

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

Cái nào sẽ tạo ra một cái cây trông như thế này ( *chỉ ra phần đầu hoặc phần cuối của một từ):

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

Chúng ta có thể kết hợp điều này thành một giải pháp bằng cách kết hợp nó với một kinh nghiệm về cách chọn từ. Ví dụ, chúng ta có thể thích các từ dài hơn các từ ngắn hơn:

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

Chúng ta có thể sử dụng chức năng này như sau:

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

Bởi vì chúng tôi duy trì vị trí của chúng tôi trong Triekhi chúng ta tìm kiếm lâu hơn và từ lâu, chúng tôi đi qua trietối đa một lần mỗi giải pháp khả thi (chứ không phải 2lần cho peanut: pea, peanut). Sự ngắn mạch cuối cùng giúp chúng ta không phải đi dây thông qua chuỗi trong trường hợp xấu nhất.

Kết quả cuối cùng chỉ là một số ít kiểm tra:

'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

Một lợi ích của giải pháp này là bạn biết rất nhanh nếu các từ dài hơn tồn tại với một tiền tố nhất định, điều này giúp loại bỏ sự cần thiết phải kiểm tra toàn diện các tổ hợp chuỗi với từ điển. Nó cũng làm chounsolvable câu trả lời tương đối rẻ so với các triển khai khác.

Nhược điểm của giải pháp này là dung lượng bộ nhớ lớn trievà chi phí xây dựng triemặt trước.


0

Nếu bạn có một danh sách đầy đủ các từ có trong chuỗi:

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

Sử dụng khả năng hiểu danh sách để lặp lại danh sách để tìm từ và số lần từ đó xuất hiện.

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

Hàm trả về kết stringquả đầu ra của các từ theo thứ tự của danh sáchtable table apple chair cupboard


0

Rất cám ơn sự trợ giúp trong https://github.com/keredson/wordninja/

Một đóng góp nhỏ tương tự trong Java từ phía tôi.

Phương thức public splitContiguousWordscó thể được nhúng với 2 phương thức khác trong lớp có ninja_words.txt trong cùng một thư mục (hoặc được sửa đổi theo sự lựa chọn của người viết mã). Và phương pháp splitContiguousWordscó thể được sử dụng cho mục đích.

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

điều gì sẽ xảy ra nếu chúng ta không có danh sách các từ?
shirazy

Nếu tôi đã hiểu đúng truy vấn: Do đó, trong cách tiếp cận ở trên, publicphương thức chấp nhận một câu thuộc loại Stringđược phân tách dựa trên mức đầu tiên với regex. Và danh sách ninja_wordscó sẵn để tải xuống từ git repo.
Arnab Das


-1

Bạn cần xác định vốn từ vựng của mình - có lẽ bất kỳ danh sách từ miễn phí nào cũng vậy.

Sau khi hoàn tất, hãy sử dụng từ vựng đó để xây dựng cây hậu tố và khớp luồng đầu vào của bạn với nó: http://en.wikipedia.org/wiki/Suffix_tree


Điều này sẽ hoạt động như thế nào trong thực tế? Sau khi xây dựng cây hậu tố, bạn sẽ biết những gì để kết hợp?
John Kurlak

@JohnKurlak Giống như bất kỳ automaton hữu hạn xác định nào khác - phần cuối của một từ hoàn chỉnh là trạng thái chấp nhận.
Marcin

Cách tiếp cận đó không yêu cầu backtracking? Bạn đã không đề cập backtracking trong câu trả lời của bạn ...
John Kurlak

Tại sao không? Điều gì xảy ra nếu bạn có "tableprechaun", như được đề cập bên dưới? Nó sẽ khớp với từ dài nhất mà nó có thể, "table", và sau đó nó sẽ không tìm thấy từ khác. Nó sẽ phải quay lại "tab" và sau đó khớp với "leprechaun".
John Kurlak

@JohnKurlak Có thể tồn tại nhiều "chi nhánh" cùng một lúc. Trên thực tế, bạn đẩy một mã thông báo vào cây cho mỗi chữ cái là một từ bắt đầu có thể có và cùng một chữ cái đó có thể thúc đẩy các mã thông báo trực tiếp khác.
Marcin
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.