Как разбить текст без пробелов на список слов? - PullRequest
89 голосов
/ 15 января 2012

Ввод: "tableapplechairtablecupboard..." много слов

Каким будет эффективный алгоритм разделения такого текста на список слов и получения:

Вывод: ["table", "apple", "chair", "table", ["cupboard", ["cup", "board"]], ...]

Первое, что приходит в голову, - это пройти через все возможные слова (начиная с первой буквы) и найти самое длинное из возможных слов, продолжить с position=word_position+len(word)

PS
У нас есть список всех возможных слов.
Слово «шкаф» может быть «чашка» и «доска», выбирайте самое длинное.
Язык: python, но главное - сам алгоритм.

Ответы [ 13 ]

153 голосов
/ 25 июля 2012

Наивный алгоритм не даст хороших результатов при применении к реальным данным. Вот алгоритм из 20 строк, который использует относительную частоту слов для получения точных результатов для текста в реальном слове.

(Если вы хотите получить ответ на свой первоначальный вопрос, в котором не используется частота слов, вам нужно уточнить, что именно означает «самое длинное слово»: лучше ли иметь слово из 20 букв и десять 3 -буквенные слова, или лучше иметь пять десятибуквенных слов? Как только вы остановитесь на точном определении, вам просто нужно изменить строку, определяющую wordcost, чтобы отразить предполагаемое значение.)

Идея

Лучший способ продолжить - это модель распределение выходных данных. Хорошим первым приближением является предположение, что все слова распределены независимо. Тогда вам нужно только знать относительную частоту всех слов. Разумно предположить, что они следуют закону Ципфа, то есть слово с рангом 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))

Результаты

Я использую этот быстрый и грязный словарь из 125 тысяч слов, который я собрал из небольшого подмножества Википедии.

До: thumbgreenappleactive присвоение неделя метафора.
После: Еженедельная метафора активного назначения зеленого яблока.

До: есть массивы текстовой информации о людях, которые отошли от языка, но не дошли до них. odelimitedcharactersinthemforexamplethumbgreenappleactiveassignmentweeklymetapho rapparentlytherearethumbgreenappleetcinthestringialsohavealargedictionarytoquery whetherthewordisreasonablesowhatsthefastestwayofextractionthxalot.

После: имеется масса текстовой информации комментариев людей, которая разбирается из html, но в них нет разделенных символов, например, метафора еженедельной метки для активного зеленого пальца, очевидно, есть зеленое яблоко большого пальца и т. Д. В Строка У меня также есть большой словарь для запроса, является ли слово разумным, так что это самый быстрый способ извлечения thx много.

До того, как: он был в темноте и во время шторма, испытывал сильные эмоции, испытывая сильное раздражение, испытывая сильное волнение, испытывая сильное волнение в течение нескольких лет.

После: была темная и штормовая ночь, дождь шел ливнем, за исключением периодических промежутков времени, когда он был остановлен сильным порывом ветра, который охватил улицы, потому что именно в Лондоне наша сцена ложь гремит по крышам домов и яростно разжигает скудное пламя ламп, борющихся с тьмой.

Как вы видите, это по сути безупречно. Самая важная часть - убедиться, что ваш список слов подготовлен к корпусу, подобному тому, с которым вы действительно столкнетесь, иначе результаты будут очень плохими.


Оптимизация

Реализация потребляет линейное количество времени и памяти, поэтому она достаточно эффективна. Если вам нужны дополнительные ускорения, вы можете построить дерево суффиксов из списка слов, чтобы уменьшить размер набора кандидатов.

Если вам нужно обработать очень большую последовательную строку, было бы разумно разделить строку, чтобы избежать чрезмерного использования памяти.Например, вы можете обработать текст блоками по 10000 символов и полями по 1000 символов с каждой стороны, чтобы избежать граничных эффектов.Это сведет к минимуму использование памяти и почти не повлияет на качество.

33 голосов
/ 21 апреля 2017

Основываясь на отличной работе в топ-ответе , я создал пакет pip для простоты использования.

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

Чтобы установить, запустите pip install wordninja.

Единственные различия незначительны.Это возвращает list вместо str, оно работает в python3, оно включает в себя список слов и правильно разбивает, даже если есть не-альфа-символы (например, подчеркивания, тире и т. Д.).

Еще раз спасибо родовому человеку!

https://github.com/keredson/wordninja

16 голосов
/ 15 января 2012

Вот решение с использованием рекурсивного поиска:

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']
10 голосов
/ 15 января 2012

Используя trie структуру данных , которая содержит список возможных слов, было бы не сложно сделать следующее:

  1. Предварительный указатель (в объединенной строке)
  2. Поиск и сохранение соответствующего узла в дереве
  3. Если у узла trie есть дочерние элементы (например, есть более длинные слова), перейдите к 1.
  4. Если у достигнутого узла нет дочерних, произошло совпадение самого длинного слова; добавьте слово (сохраненное в узле или просто объединенное во время обхода по дереву) в список результатов, сбросьте указатель в дереве (или сбросьте ссылку) и начните сначала
8 голосов
/ 23 января 2014

Решение Unutbu было довольно близко, но я нахожу код трудным для чтения, и он не дал ожидаемого результата. Недостаток универсального решения для человека заключается в том, что ему нужны частоты слов. Не подходит для всех случаев использования.

Вот простое решение с использованием алгоритма Divide and Conquer .

  1. Он пытается минимизировать количество слов Например. find_words('cupboard') вернет ['cupboard'] вместо ['cup', 'board'] (при условии, что cupboard, cup и board находятся в словаре)
  2. Оптимальное решение - не уникально , реализация ниже возвращает a решение. 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

Это займет около 5 секунд на моей машине 3GHz:

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

reis массы текстовой информации комментариев людей, которая разбирается из html, но в них нет символа-разделителя, например, большого пальца, зеленое яблоко, активное назначение, недельная метафора, по-видимому, в строке есть зеленое яблоко и т.д. спросить, является ли слово разумным, так какой самый быстрый способ извлечения thxa lot

6 голосов
/ 27 мая 2016

Ответ https://stackoverflow.com/users/1515832/generic-human великолепен. Но лучшая реализация этого, которую я когда-либо видел, была написана самим Питером Норвигом в его книге «Красивые данные».

Прежде чем я вставлю его код, позвольте мне подробнее остановиться на том, почему метод Норвига более точен (хотя и немного медленнее и длиннее в плане кода).

1) Данные немного лучше - как с точки зрения размера, так и с точки зрения точности (он использует количество слов вместо простого ранжирования) 2) Что еще более важно, именно логика, лежащая в основе n-граммов, действительно делает подход таким точным.

Пример, который он приводит в своей книге, - это проблема разбиения строки «сидячей». Теперь не-биграмный метод разделения строк будет учитывать p ('sit') * p ('down'), и если это значение меньше p ('sitdown') - что будет происходить довольно часто - он НЕ будет расщепляться это, но мы бы этого хотели (большую часть времени).

Однако, когда у вас есть модель биграммы, вы можете оценить p («сесть») как биграмм против p («присед»), и первые выигрывают. По сути, если вы не используете биграммы, то вероятность того, что слова разделяются, рассматривается как независимая, а это не так, некоторые слова с большей вероятностью будут появляться одно за другим. К сожалению, это также слова, которые часто слипаются во многих случаях и приводят в замешательство сплиттер.

Вот ссылка на данные (это данные для 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 
2 голосов
/ 15 января 2012

Если вы предварительно скомпилируете список слов в DFA (который будет очень медленным), то время, необходимое для согласования ввода, будет пропорционально длине строки (на самом деле, только немногомедленнее, чем просто итерация по строке).

Это, по сути, более общая версия алгоритма trie, которая упоминалась ранее.Я упоминаю это только для полной информации - пока нет реализации DFA, которую вы можете просто использовать. RE2 будет работать, но я не знаю, позволят ли привязки Python настроить размер DFA до того, как он просто выбрасывает скомпилированные данные DFA и выполняет поиск NFA.

1 голос
/ 07 ноября 2018

Вот принятый ответ, переведенный в 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();
}
0 голосов
/ 23 августа 2018

Расширение @ 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 голосов
/ 28 июня 2018

Для немецкого языка есть CharSplit, который использует машинное обучение и работает довольно хорошо для строк из нескольких слов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...