จะแบ่งข้อความโดยไม่เว้นวรรคเป็นรายการคำได้อย่างไร?


109

อินพุต: "tableapplechairtablecupboard..."หลายคำ

อะไรคืออัลกอริทึมที่มีประสิทธิภาพในการแยกข้อความดังกล่าวออกเป็นรายการคำและรับ:

เอาท์พุต: ["table", "apple", "chair", "table", ["cupboard", ["cup", "board"]], ...]

สิ่งแรกที่นึกถึงคือการอ่านคำที่เป็นไปได้ทั้งหมด (เริ่มต้นด้วยตัวอักษรตัวแรก) และค้นหาคำที่ยาวที่สุดเท่าที่จะเป็นไปได้ position=word_position+len(word)

ปล.
เรามีรายการคำที่เป็นไปได้ทั้งหมด
คำว่า "ตู้" สามารถเป็น "ถ้วย" และ "กระดาน" เลือกที่ยาวที่สุด
ภาษา: python แต่สิ่งสำคัญคืออัลกอริทึมเอง


14
แน่ใจหรือว่าสตริงไม่ได้ขึ้นต้นด้วยคำว่า "tab" และ "leap"
Rob Hruska

ใช่ดูเหมือนว่าจะไม่สามารถทำได้อย่างชัดเจน
demalexx

@RobHruska ในกรณีนั้นฉันเขียนโดยเลือกที่ยาวที่สุด
Sergey

2
@Sergey - เกณฑ์ "ยาวที่สุดที่เป็นไปได้" ของคุณบอกเป็นนัยว่าเป็นคำประสม และในกรณีนี้จะเกิดอะไรขึ้นถ้าสตริงเป็น "คาร์เรล" มันจะเป็น "พรม" หรือ "สัตว์เลี้ยง"?
Rob Hruska

2
สตริงของคุณมีคำศัพท์ตามคำบอกมากมาย:['able', 'air', 'apple', 'boa', 'boar', 'board', 'chair', 'cup', 'cupboard', 'ha', 'hair', 'lea', 'leap', 'oar', 'tab', 'table', 'up']
reclosedev

คำตอบ:


204

อัลกอริทึมที่ไร้เดียงสาจะไม่ให้ผลลัพธ์ที่ดีเมื่อนำไปใช้กับข้อมูลในโลกแห่งความเป็นจริง นี่คืออัลกอริทึม 20 บรรทัดที่ใช้ประโยชน์จากความถี่ของคำที่สัมพันธ์กันเพื่อให้ผลลัพธ์ที่ถูกต้องสำหรับข้อความคำจริง

(หากคุณต้องการคำตอบสำหรับคำถามเดิมของคุณที่ไม่ใช้ความถี่ของคำคุณจำเป็นต้องปรับแต่งความหมายของคำว่า "คำที่ยาวที่สุด": จะดีกว่าไหมหากมีคำที่มีอักษร 20 ตัวและคำ 3 ตัวอักษรสิบคำหรือเป็น จะดีกว่าถ้ามีคำศัพท์ 10 ตัวอักษรห้าคำเมื่อคุณกำหนดคำจำกัดความที่แม่นยำแล้วคุณก็ต้องเปลี่ยนการกำหนดบรรทัดwordcostเพื่อให้สอดคล้องกับความหมายที่ตั้งใจไว้)

ความคิด

วิธีที่ดีที่สุดในการดำเนินการคือการจำลองการกระจายของผลลัพธ์ การประมาณอย่างแรกที่ดีคือสมมติว่าทุกคำมีการกระจายอย่างอิสระ จากนั้นคุณจะต้องทราบความถี่สัมพัทธ์ของคำทั้งหมดเท่านั้น มีเหตุผลที่จะถือว่าพวกเขาปฏิบัติตามกฎของ Zipf นั่นคือคำที่มีอันดับnในรายการคำที่มีความน่าจะเป็นประมาณ 1 / ( n log N ) โดยที่Nคือจำนวนคำในพจนานุกรม

เมื่อคุณแก้ไขโมเดลแล้วคุณสามารถใช้การเขียนโปรแกรมแบบไดนามิกเพื่อสรุปตำแหน่งของช่องว่างได้ ประโยคที่เป็นไปได้มากที่สุดคือประโยคที่เพิ่มผลคูณของความน่าจะเป็นของแต่ละคำให้มากที่สุดและง่ายต่อการคำนวณด้วยโปรแกรมไดนามิก แทนที่จะใช้ความน่าจะเป็นโดยตรงเราใช้ต้นทุนที่กำหนดเป็นลอการิทึมของผกผันของความน่าจะเป็นเพื่อหลีกเลี่ยงการล้น

รหัส

from math import log

# Build a cost dictionary, assuming Zipf's law and cost = -math.log(probability).
words = open("words-by-frequency.txt").read().split()
wordcost = dict((k, log((i+1)*log(len(words)))) for i,k in enumerate(words))
maxword = max(len(x) for x in words)

def infer_spaces(s):
    """Uses dynamic programming to infer the location of spaces in a string
    without spaces."""

    # Find the best match for the i first characters, assuming cost has
    # been built for the i-1 first characters.
    # Returns a pair (match_cost, match_length).
    def best_match(i):
        candidates = enumerate(reversed(cost[max(0, i-maxword):i]))
        return min((c + wordcost.get(s[i-k-1:i], 9e999), k+1) for k,c in candidates)

    # Build the cost array.
    cost = [0]
    for i in range(1,len(s)+1):
        c,k = best_match(i)
        cost.append(c)

    # Backtrack to recover the minimal-cost string.
    out = []
    i = len(s)
    while i>0:
        c,k = best_match(i)
        assert c == cost[i]
        out.append(s[i-k:i])
        i -= k

    return " ".join(reversed(out))

ซึ่งคุณสามารถใช้กับ

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

ผลลัพธ์

ฉันใช้พจนานุกรม 125k คำที่รวดเร็วและสกปรกที่ฉันรวบรวมจาก Wikipedia ชุดย่อย ๆ

Before: thumbgreenappleactiveassignmentweeklymetaphor.
After:แอปเปิ้ลเขียวนิ้วหัวแม่มืออุปมาประจำสัปดาห์

Before: thereismassesoftextinofpeoplescommentswhichisparsedfromhtmlbuttherearen odelimited อักขระinthemforexample Thumb สีเขียวแอปเปิ้ลการมอบหมายสัปดาห์ metapho rapparentlytherearethumbgreenappleetcinthestringialsohavealarged พจนานุกรม

หลังจาก:มีข้อมูลข้อความจำนวนมากของความคิดเห็นของผู้คนซึ่งแยกวิเคราะห์จาก html แต่ไม่มีอักขระที่ใช้ตัวคั่นในตัวอย่างเช่นแอปเปิ้ลเขียวนิ้วหัวแม่มือการกำหนดใช้งานการอุปมาอุปมัยรายสัปดาห์เห็นได้ชัดว่ามีแอปเปิ้ลเขียวนิ้วหัวแม่มือ ฯลฯ ในสตริงฉันยังมีพจนานุกรมขนาดใหญ่ สอบถามว่าคำนั้นสมเหตุสมผลหรือไม่ดังนั้นวิธีใดจึงเป็นวิธีที่เร็วที่สุดในการดึงข้อมูลออกมาก

Before: itwasadarkandstormynight therainfellintorrentsexceptatoccasional intervalswhenit waschecked byaviolentgustofwindwhichswepthestreetsforitisinlondonthatoursceneliesrattlingalongthehousetopsandfiercelyagitatethescantyflameofthelampsthatstruggledagledagel

หลังจาก:เป็นคืนที่มืดและมีพายุฝนตกลงมาอย่างกระหน่ำยกเว้นในบางครั้งบางคราวเมื่อมีการตรวจสอบโดยลมกระโชกรุนแรงซึ่งพัดไปตามท้องถนนเนื่องจากอยู่ในลอนดอนฉากของเราอยู่อย่างครึกโครมไปตามหลังคาเรือนและก่อกวนอย่างดุเดือด เปลวไฟที่ไม่เพียงพอของตะเกียงที่ต่อสู้กับความมืด

อย่างที่คุณเห็นมันไร้ที่ติเป็นหลัก ส่วนที่สำคัญที่สุดคือการตรวจสอบให้แน่ใจว่ารายการคำศัพท์ของคุณได้รับการฝึกฝนในคลังข้อมูลที่คล้ายกับสิ่งที่คุณจะพบจริงมิฉะนั้นผลลัพธ์จะแย่มาก


การเพิ่มประสิทธิภาพ

การใช้งานใช้เวลาและหน่วยความจำเชิงเส้นดังนั้นจึงมีประสิทธิภาพพอสมควร หากคุณต้องการเพิ่มความเร็วเพิ่มเติมคุณสามารถสร้างโครงสร้างคำต่อท้ายจากรายการคำเพื่อลดขนาดของชุดผู้สมัคร

หากคุณต้องการประมวลผลสตริงที่มีขนาดใหญ่มากติดต่อกันการแยกสตริงเพื่อหลีกเลี่ยงการใช้หน่วยความจำมากเกินไปก็สมควร ตัวอย่างเช่นคุณสามารถประมวลผลข้อความในบล็อกที่มีอักขระ 10,000 ตัวบวกระยะขอบ 1,000 อักขระในด้านใดด้านหนึ่งเพื่อหลีกเลี่ยงผลกระทบจากขอบเขต วิธีนี้จะทำให้การใช้หน่วยความจำน้อยที่สุดและแทบจะไม่มีผลต่อคุณภาพ


1
ข้อความสองบรรทัดล่ะ
leafiy

11
รหัสนี้ทำให้ฉันมึน ฉันไม่เข้าใจสักนิด ฉันไม่เข้าใจสิ่งที่บันทึก แต่ฉันทดสอบรหัสนี้บนคอมพิวเตอร์ของฉัน คุณคืออัจฉริยะ.
Aditya Singh

1
เวลาทำงานของอัลกอริทึมนี้คืออะไร? ทำไมคุณไม่ใช้ ahocorasick?
RetroCode

9
ยอดเยี่ยมมาก ฉันได้กลายเป็นแพคเกจจุดเล็ก: 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

53

จากผลงานที่ยอดเยี่ยมในคำตอบด้านบนฉันได้สร้าง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"
Harry M

@keredson - ก่อนอื่นขอบคุณสำหรับการแก้ปัญหา มันทำงานได้ดี อย่างไรก็ตามมันจะตัดอักขระพิเศษเช่น "-" เป็นต้นบางครั้งก็ไม่ได้ให้การแบ่งที่เหมาะสมเช่นใช้สตริงยาว ๆ พูดว่า - "WeatheringPropertiesbyMaterial Trade Name Graph 2-1. Color Change, E, หลังจาก Arizona, Florida, Cycolac® / Geloy® Resin Systems เมื่อเทียบกับ PVC [15] 25 20 15 ∆E 10 5 0 PVC, White PVC, Brown C / G, BrownC / G Capstock เป็นวัสดุที่ใช้เป็นชั้นพื้นผิวที่ใช้กับพื้นผิวภายนอกของโปรไฟล์ การอัดขึ้นรูปปลอกหุ้มเรซินGeloy®บนพื้นผิวCycolac®ช่วยให้สามารถทนต่อสภาพอากาศได้ดีเยี่ยม [25] "
Rakesh Lamp Stack

คุณเปิดปัญหาใน GH ได้ไหม
keredson

1
เยี่ยมมากขอบคุณสำหรับความพยายาม ช่วยประหยัดเวลาได้มากจริงๆ
Jan Zeiseweis

17

นี่คือวิธีแก้ปัญหาโดยใช้การค้นหาแบบเรียกซ้ำ:

def find_words(instring, prefix = '', words = None):
    if not instring:
        return []
    if words is None:
        words = set()
        with open('/usr/share/dict/words') as f:
            for line in f:
                words.add(line.strip())
    if (not prefix) and (instring in words):
        return [instring]
    prefix, suffix = prefix + instring[0], instring[1:]
    solutions = []
    # Case 1: prefix in solution
    if prefix in words:
        try:
            solutions.append([prefix] + find_words(suffix, '', words))
        except ValueError:
            pass
    # Case 2: prefix not in solution
    try:
        solutions.append(find_words(suffix, prefix, words))
    except ValueError:
        pass
    if solutions:
        return sorted(solutions,
                      key = lambda solution: [len(word) for word in solution],
                      reverse = True)[0]
    else:
        raise ValueError('no solution')

print(find_words('tableapplechairtablecupboard'))
print(find_words('tableprechaun', words = set(['tab', 'table', 'leprechaun'])))

ผลตอบแทน

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

ใช้งานได้ "นอกกรอบ" ขอบคุณ! ฉันคิดว่าจะใช้ trie structure ตามที่มิคุพูดไม่ใช่แค่กำหนดทุกคำ ขอบคุณต่อไป!
Sergey

11

การใช้โครงสร้างข้อมูลtrie ซึ่งเก็บรายชื่อคำที่เป็นไปได้มันจะไม่ซับซ้อนเกินไปที่จะทำสิ่งต่อไปนี้:

  1. ตัวชี้ล่วงหน้า (ในสตริงที่ต่อกัน)
  2. ค้นหาและจัดเก็บโหนดที่เกี่ยวข้องในสาม
  3. หาก trie node มีลูก (เช่นมีคำที่ยาวกว่า) ให้ไปที่ 1
  4. หากโหนดไปถึงไม่มีลูกจะเกิดการจับคู่คำที่ยาวที่สุด เพิ่มคำ (เก็บไว้ในโหนดหรือเพียงแค่เชื่อมต่อระหว่างการข้ามสามครั้ง) ลงในรายการผลลัพธ์รีเซ็ตตัวชี้ในสาม (หรือรีเซ็ตการอ้างอิง) และเริ่มต้นใหม่

3
หากเป้าหมายคือการใช้สตริงทั้งหมดคุณจะต้องย้อนกลับ"tableprechaun"จากนั้นจะต้องแยกตามหลัง"tab"แล้วจะต้องมีการแยกหลัง
Daniel Fischer

นอกจากนี้สำหรับการพูดถึง trie แต่ฉันก็เห็นด้วยกับ Daniel ว่าต้องทำ backtracking
Sergey

@ แดเนียลการค้นหาที่ตรงกันยาวที่สุดไม่จำเป็นต้องมีการย้อนกลับไม่ใช่ อะไรทำให้คุณคิดอย่างนั้น? แล้วอัลกอริทึมด้านบนผิดอะไร?
Devin Jeanpierre

1
@ เดวินความจริงที่ว่าสำหรับ"tableprechaun"การจับคู่ที่ยาวนานที่สุดตั้งแต่เริ่มต้นคือการ"table"จากไป"prechaun"ซึ่งไม่สามารถแยกออกเป็นคำในพจนานุกรมได้ ดังนั้นคุณต้องทำการจับคู่ที่สั้นกว่าโดย"tab"ทิ้งไฟล์"leprechaun".
Daniel Fischer

@ แดเนียลขอโทษใช่ ฉันเข้าใจปัญหาผิด อัลกอริทึมที่ได้รับการแก้ไขควรติดตามตำแหน่งทรีที่เป็นไปได้ทั้งหมดพร้อมกัน - การค้นหา NFA เชิงเส้นเวลา AKA หรืออย่างอื่นย้อนรอยได้ แต่นั่นคือเวลาเอ็กซ์โปเนนเชียลกรณีที่เลวร้ายที่สุด
Devin Jeanpierre

9

โซลูชันของ Unutbu ค่อนข้างใกล้เคียง แต่ฉันพบว่ารหัสอ่านยากและไม่ได้ผลลัพธ์ที่คาดหวัง วิธีแก้ปัญหาของ Generic Human มีข้อเสียเปรียบคือต้องใช้ความถี่ของคำ ไม่เหมาะสำหรับทุกกรณีการใช้งาน

นี่คือทางออกที่ง่ายโดยใช้แบ่งและขั้นตอนวิธีการปราบ

  1. มันพยายามที่จะลดจำนวนของคำเช่นfind_words('cupboard')จะกลับมา['cupboard']มากกว่า['cup', 'board'](สมมติว่าcupboard, cupและboardอยู่ใน dictionnary) ที่
  2. ทางออกที่ดีที่สุดคือไม่ซ้ำกัน , การดำเนินการดังต่อไปนี้ผลตอบแทนวิธีการแก้ปัญหา สามารถกลับมาหรืออาจจะกลับมา(ดังที่เห็นด้านล่าง) คุณสามารถแก้ไขอัลกอริทึมได้อย่างง่ายดายเพื่อส่งคืนโซลูชันที่ดีที่สุดทั้งหมดfind_words('charactersin')['characters', 'in']['character', 'sin']
  3. ในการดำเนินการนี้จะแก้ปัญหาmemoizedเพื่อที่จะทำงานในเวลาที่เหมาะสม

รหัส:

words = set()
with open('/usr/share/dict/words') as f:
    for line in f:
        words.add(line.strip())

solutions = {}
def find_words(instring):
    # First check if instring is in the dictionnary
    if instring in words:
        return [instring]
    # No... But maybe it's a result we already computed
    if instring in solutions:
        return solutions[instring]
    # Nope. Try to split the string at all position to recursively search for results
    best_solution = None
    for i in range(1, len(instring) - 1):
        part1 = find_words(instring[:i])
        part2 = find_words(instring[i:])
        # Both parts MUST have a solution
        if part1 is None or part2 is None:
            continue
        solution = part1 + part2
        # Is the solution found "better" than the previous one?
        if best_solution is None or len(solution) < len(best_solution):
            best_solution = solution
    # Remember (memoize) this solution to avoid having to recompute it
    solutions[instring] = best_solution
    return best_solution

จะใช้เวลาประมาณ 5 วินาทีในเครื่อง 3GHz ของฉัน:

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

ข้อมูลข้อความจำนวนมากของความคิดเห็นของผู้คนซึ่งแยกวิเคราะห์จาก html แต่ไม่มีอักขระที่ใช้ตัวคั่นบาปตัวอย่างเช่นแอปเปิ้ลสีเขียวนิ้วหัวแม่มือการกำหนดใช้งานประจำสัปดาห์อุปมาดูเหมือนว่ามีแอปเปิ้ลสีเขียวนิ้วหัวแม่มือ ฯลฯ ในสตริงฉันยังมีพจนานุกรมขนาดใหญ่เพื่อสอบถามว่า คำนี้สมเหตุสมผลดังนั้นวิธีที่เร็วที่สุดในการสกัด thxa lot คืออะไร


ไม่มีเหตุผลที่จะเชื่อว่าข้อความไม่สามารถลงท้ายด้วยตัวอักษรคำเดียว คุณควรพิจารณาอีกหนึ่งแยก
panda-34

7

คำตอบโดย https://stackoverflow.com/users/1515832/generic-humanนั้นยอดเยี่ยมมาก แต่การนำสิ่งนี้ไปใช้ได้ดีที่สุดที่ฉันเคยเห็นมาคือ Peter Norvig เขียนด้วยตัวเองในหนังสือ 'Beautiful Data'

ก่อนที่ฉันจะวางโค้ดของเขาให้ฉันขยายความว่าทำไมวิธีการของ Norvig จึงแม่นยำกว่า (แม้ว่าจะช้ากว่าเล็กน้อยและนานกว่าในแง่ของโค้ด)

1) ข้อมูลดีขึ้นเล็กน้อย - ทั้งในแง่ของขนาดและในแง่ของความแม่นยำ (เขาใช้การนับจำนวนคำมากกว่าการจัดอันดับแบบธรรมดา) 2) ที่สำคัญกว่านั้นคือตรรกะที่อยู่เบื้องหลัง n-g ที่ทำให้แนวทางนั้นแม่นยำจริงๆ .

ตัวอย่างที่เขาให้ไว้ในหนังสือของเขาคือปัญหาของการแยกสตริง 'sitdown' ตอนนี้วิธีการแยกสตริงที่ไม่ใช่ bigram จะพิจารณา p ('sit') * p ('down') และถ้าน้อยกว่า p ('sitdown') ซึ่งเป็นกรณีที่ค่อนข้างบ่อย - มันจะไม่แยก แต่เราต้องการให้ (เกือบตลอดเวลา)

อย่างไรก็ตามเมื่อคุณมีโมเดล bigram คุณสามารถกำหนดค่า p ('นั่งลง') เป็น bigram vs 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
Harry M

ngrams จะช่วยเพิ่มความแม่นยำให้กับคุณได้อย่างแน่นอนด้วยการใช้คำสั่งความถี่หน่วยความจำและการคำนวณที่ใหญ่ขึ้นอย่างทวีคูณ btw ฟังก์ชั่นบันทึกช่วยจำรั่วหน่วยความจำเหมือนตะแกรงที่นั่น ควรล้างระหว่างการโทร
keredson

3

นี่คือคำตอบที่ยอมรับซึ่งแปลเป็น JavaScript (ต้องใช้ node.js และไฟล์ "wordninja_words.txt" จาก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

หากคุณรวบรวมรายการคำไว้ล่วงหน้าในDFA (ซึ่งจะช้ามาก) เวลาที่ใช้ในการจับคู่อินพุตจะเป็นสัดส่วนกับความยาวของสตริง (อันที่จริงช้ากว่าการวนซ้ำบนสตริงเพียงเล็กน้อย)

นี่เป็นอัลกอริทึม Trie เวอร์ชันทั่วไปที่มีประสิทธิภาพมากขึ้นซึ่งได้กล่าวไว้ก่อนหน้านี้ ฉันพูดถึงมันโดยสมบูรณ์เท่านั้น - ณ ตอนนี้ยังไม่มีการใช้งาน DFA ที่คุณสามารถใช้ได้ RE2จะใช้งานได้ แต่ฉันไม่รู้ว่าการผูก Python ให้คุณปรับขนาดใหญ่แค่ไหนที่คุณอนุญาตให้ DFA มีขนาดใหญ่ก่อนที่มันจะทิ้งข้อมูล DFA ที่คอมไพล์แล้วและทำการค้นหา NFA


โดยเฉพาะอย่างยิ่งสำหรับ re2 ไม่เคยใช้มาก่อน
Sergey

0

ดูเหมือนว่าการย้อนรอยทางโลกธรรมจะทำ เริ่มต้นที่การเรียกร้องของสตริง สแกนไปทางขวาจนกว่าคุณจะมีคำ จากนั้นเรียกใช้ฟังก์ชันในส่วนที่เหลือของสตริง ฟังก์ชันจะส่งคืน "เท็จ" หากสแกนไปทางขวาจนสุดโดยที่จำคำศัพท์ไม่ได้ มิฉะนั้นจะส่งคืนคำที่พบและรายการคำที่ส่งกลับโดยการเรียกซ้ำ

ตัวอย่าง: "tableapple" ค้นหา "tab" จากนั้น "leap" แต่ไม่มีคำว่า "ple" ไม่มีคำอื่นใน "leapple" ค้นหา "table" ตามด้วย "app" "le" ไม่ใช่สักคำก็เลยลอง apple รับรู้ผลตอบแทน

เพื่อให้ได้ระยะเวลาที่ยาวนานที่สุดให้ดำเนินการต่อไปเพียงแค่ปล่อยวิธีแก้ปัญหาที่ถูกต้อง (แทนที่จะส่งคืน) จากนั้นเลือกเกณฑ์ที่เหมาะสมที่สุดตามเกณฑ์ที่คุณเลือก (maxmax, minmax, average ฯลฯ )


อัลกอริทึมที่ดีกำลังคิดเกี่ยวกับมัน unutbu เขียนโค้ดด้วยซ้ำ
Sergey

@Sergey การค้นหาย้อนรอยเป็นอัลกอริทึมเวลาเอ็กซ์โปเนนเชียล "ดี" เกี่ยวกับอะไร?
Devin Jeanpierre

1
มันง่ายมากไม่ได้บอกว่ามันเร็ว
Sergey

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 ในการใช้ a 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สามารถฝังไว้กับอีก 2 วิธีในคลาสที่มี ninja_words.txt ในไดเร็กทอรีเดียวกัน (หรือแก้ไขตามตัวเลือกของ coder) และวิธีการนี้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ซึ่งแยกตามระดับแรกด้วย regex และสำหรับรายชื่อninja_wordsนั้นสามารถดาวน์โหลดได้จาก git repo
Arnab Das


-1

คุณจำเป็นต้องระบุคำศัพท์ของคุณ - บางทีอาจมีรายการคำศัพท์ฟรี

เมื่อเสร็จแล้วให้ใช้คำศัพท์นั้นเพื่อสร้างโครงสร้างคำต่อท้ายและจับคู่สตรีมอินพุตของคุณกับสิ่งนั้น: http://en.wikipedia.org/wiki/Suffix_tree


วิธีนี้จะได้ผลในทางปฏิบัติ? หลังจากสร้างต้นไม้ต่อท้ายคุณจะรู้ได้อย่างไรว่าจะจับคู่อะไร
John Kurlak

@JohnKurlak เช่นเดียวกับหุ่นยนต์กำหนดขอบเขต จำกัด อื่น ๆ - การสิ้นสุดคำที่สมบูรณ์คือสถานะที่ยอมรับ
Marcin

วิธีนี้ไม่จำเป็นต้องมีการย้อนรอย? คุณไม่ได้กล่าวถึงการย้อนรอยในคำตอบของคุณ ...
John Kurlak

ทำไมจะไม่ล่ะ? จะเกิดอะไรขึ้นถ้าคุณมี "tableprechaun" ดังที่กล่าวไว้ด้านล่าง มันจะจับคู่คำที่ยาวที่สุดที่มันสามารถ "ตาราง" และจากนั้นจะไม่พบคำอื่น จะต้องย้อนกลับไปที่ "แท็บ" แล้วจับคู่ "ผีแคระ"
John Kurlak

@JohnKurlak "หลายสาขา" สามารถอยู่ได้ในเวลาเดียวกัน ส่งผลให้คุณดันโทเค็นลงในแผนภูมิสำหรับตัวอักษรทุกตัวซึ่งเป็นการขึ้นต้นของคำที่เป็นไปได้และตัวอักษรเดียวกันอาจเลื่อนโทเค็นสดอื่น ๆ
Marcin
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.