Функция N-грамм в vb.net - ›создавать граммы для слов вместо символов

Недавно я узнал о n-граммах и классной возможности сравнивать с ними частоту фраз в теле текста. Теперь я пытаюсь создать приложение vb.net, которое просто получает тело текста и возвращает список наиболее часто используемых фраз (где n> = 2).

Я нашел пример C # того, как сгенерировать n-грамм из тела текста, поэтому я начал с преобразования кода в VB. Проблема в том, что этот код создает один грамм на символ вместо одного грамма на слово. Для слов я хочу использовать следующие разделители: VbCrLf (новая строка), vbTab (табуляторы) и следующие символы:! @ # $% ^ & * () _ + - = {} | \: \ "'? ¿ /., ‹> '¡º × ÷';« »[]

Есть ли у кого-нибудь идеи, как я могу переписать для этой цели следующую функцию:

   Friend Shared Function GenerateNGrams(ByVal text As String, ByVal gramLength As Integer) As String()
    If text Is Nothing OrElse text.Length = 0 Then
        Return Nothing
    End If

    Dim grams As New ArrayList()
    Dim length As Integer = text.Length
    If length < gramLength Then
        Dim gram As String
        For i As Integer = 1 To length
            gram = text.Substring(0, (i) - (0))
            If grams.IndexOf(gram) = -1 Then
                grams.Add(gram)
            End If
        Next

        gram = text.Substring(length - 1, (length) - (length - 1))
        If grams.IndexOf(gram) = -1 Then
            grams.Add(gram)

        End If
    Else
        For i As Integer = 1 To gramLength - 1
            Dim gram As String = text.Substring(0, (i) - (0))
            If grams.IndexOf(gram) = -1 Then
                grams.Add(gram)

            End If
        Next

        For i As Integer = 0 To (length - gramLength)
            Dim gram As String = text.Substring(i, (i + gramLength) - (i))
            If grams.IndexOf(gram) = -1 Then
                grams.Add(gram)
            End If
        Next

        For i As Integer = (length - gramLength) + 1 To length - 1
            Dim gram As String = text.Substring(i, (length) - (i))
            If grams.IndexOf(gram) = -1 Then
                grams.Add(gram)
            End If
        Next
    End If
    Return Tokeniser.ArrayListToArray(grams)
End Function

person Majgel    schedule 10.03.2010    source источник


Ответы (2)


n -грамма для слов - это просто список длиной n, в котором хранятся эти слова. В этом случае список n -грамм представляет собой просто список слов. Если вы хотите сохранить частоту, вам понадобится словарь, который индексируется этими n -граммами. Для вашего особого случая с 2 граммами вы можете представить себе что-то вроде этого:

Dim frequencies As New Dictionary(Of String(), Integer)(New ArrayComparer(Of String)())
Const separators as String = "!@#$%^&*()_+-={}|\:""'?¿/.,<>’¡º×÷‘;«»[] " & _
                             ControlChars.CrLf & ControlChars.Tab
Dim words = text.Split(separators.ToCharArray(), StringSplitOptions.RemoveEmptyEntries)

For i As Integer = 0 To words.Length - 2
    Dim ngram = New String() { words(i), words(i + 1) }
    Dim oldValue As Integer = 0
    frequencies.TryGetValue(ngram, oldValue)
    frequencies(ngram) = oldValue + 1
Next

frequencies теперь должен содержать словарь со всеми двумя последовательными парами слов, содержащимися в тексте, и частотой, с которой они появляются (как последовательная пара).

Для этого кода требуется класс ArrayComparer:

Public Class ArrayComparer(Of T)
    Implements IEqualityComparer(Of T())

    Private ReadOnly comparer As IEqualityComparer(Of T)

    Public Sub New()
        Me.New(EqualityComparer(Of T).Default)
    End Sub

    Public Sub New(ByVal comparer As IEqualityComparer(Of T))
        Me.comparer = comparer
    End Sub

    Public Overloads Function Equals(ByVal a As T(), ByVal b As T()) As Boolean _
            Implements IEqualityComparer(Of T()).Equals
        System.Diagnostics.Debug.Assert(a.Length = b.Length)
        For i As Integer = 0 to a.Length - 1
            If Not comparer.Equals(a(i), b(i)) Then Return False
        Next

        Return True
    End Function

    Public Overloads Function GetHashCode(ByVal arr As T()) As Integer _
            Implements IEqualityComparer(Of T()).GetHashCode
        Dim hashCode As Integer = 17
        For Each obj As T In arr
            hashCode = ((hashCode << 5) - 1) Xor comparer.GetHashCode(obj)
        Next

        Return hashCode
    End Function
End Class

К сожалению, этот код не компилируется в Mono, потому что компилятор VB не может найти общий класс EqualityComparer. Поэтому я не могу проверить, работает ли GetHashCode реализацияw должным образом, но все должно быть в порядке.

person Konrad Rudolph    schedule 10.03.2010
comment
Супер спасибо! Полный ответ и предстоящие вопросы в сообщении ниже: - person Majgel; 11.03.2010
comment
У меня проблемы с запуском этого кода. Когда я попробовал это в VS2008, я сначала получил предупреждение, что функции Equals и GetHashCode затеняют переопределяемый метод в базовом классе Object. Чтобы переопределить базовый метод, этот метод должен быть объявлен как «Переопределение». При запуске код прерывается на частотах.Add (ngram, oldValue + 1) за исключением. Элемент с таким же ключом уже был добавлен при попытке вставить второе вхождение того же ngram. - person Majgel; 15.03.2010
comment
@Majgel: затенение действительно является проблемой. Как я уже писал, я не могу протестировать код. Чтобы исправить ошибку, просто добавьте Overloads. Я поправил ответ. Исправлена ​​и другая ошибка. - person Konrad Rudolph; 15.03.2010
comment
Еще раз спасибо за быстрый ответ! Теперь предупреждение VS2008 исчезло. Однако описанный выше разрыв кода все еще существует. Это происходит только тогда, когда пытается вставить n-грамм, который уже существует в словаре. Обе частоты.TryGetValue (ngram, oldValue) и frequency.Add (ngram, oldValue + 1) запускают функцию класса Equals () и возвращают True, как предполагалось. Затем появляется исключение. Элемент с таким ключом уже был добавлен. Любые идеи? - person Majgel; 15.03.2010
comment
@Majgel: Я также изменил этот фрагмент кода: вместо Add метод теперь использует индексированный метод доступа, то есть frequencies(ngram) = oldValue + 1. - person Konrad Rudolph; 15.03.2010

Большое спасибо Конраду за начало решения!

Я попробовал ваш код и получил следующий результат:

Text = "Hello I am a test Also I am a test"
(I also included whitespace as a separator)

frequencies now has 9 items:
---------------------
Keys: "Hello", "I"
Value: 1
---------------------
Keys: "I", "am"
Value: 1
---------------------
Keys: "am", "a"
Value: 1
---------------------
Keys: "a", "test"
Value: 1
---------------------
Keys: "test", "Also"
Value: 1
---------------------
Keys: "Also", "I"
Value: 1
---------------------
Keys: "I", "am"
Value: 1
---------------------
Keys: "am", "a"
Value: 1
---------------------
Keys: "a", "test"
Value: 1
---------------------

Мой первый вопрос: разве 3 последние пары ключей не должны иметь значение 2, поскольку они встречаются в тексте два раза?

Во-вторых: причина, по которой я выбрал подход n-граммов, заключается в том, что я не хочу ограничивать количество слов (n) определенной длиной. Есть ли способ применить динамический подход, который пытается сначала найти самое длинное совпадение фразы, а затем перейти к последнему количеству слов, равному 2?

Мой целевой результат для приведенного выше примера запроса:

---------------------
Match: "I am a test"
Frequency: 2
---------------------
Match: "I am a"
Frequency: 2
---------------------
Match: "am a test"
Frequency: 2
---------------------
Match: "I am"
Frequency: 2
---------------------
Match: "am a"
Frequency: 2
---------------------
Match: "a test"
Frequency: 2
---------------------

Для этого существует аналогичный подход C ++, написанный Хатемом Мостафой на codeproject.com: N- грамма и алгоритм быстрого извлечения паттернов

К сожалению, я не специалист по C ++ и понятия не имею, как преобразовать этот фрагмент кода, поскольку он включает в себя много операций с памятью, которых нет в .NET. Единственная проблема с этим примером заключается в том, что вам нужно указать минимальную длину шаблона слова, и я хочу, чтобы она была динамической от 2 до максимального найденного.

person Majgel    schedule 10.03.2010
comment
Во-первых, чтобы исправить ошибку: ЧЕРТ! Судя по всему, метод массивов GetHashCode не работает. К сожалению, этот метод используется внутри словаря. В этом причина странного результата. См. Мой обновленный ответ для обходного пути: нам нужно определить класс, который выполняет правильное сравнение массивов на равенство. - person Konrad Rudolph; 11.03.2010
comment
О другой вашей проблеме: я не уверен, что вы ожидаете в результате. Максимально возможный n-грамм, встречающийся более одного раза? Я думаю, что это будет некоторая вариация самой длинной общей проблемы подпоследовательности, и, как таковая, она будет NP сложной, то есть неэффективно разрешимой, поскольку она не просто влечет за собой две различные последовательности, а скорее все возможные подпоследовательности из исходной последовательности, из которых их экспоненциально много. - person Konrad Rudolph; 11.03.2010
comment
И последнее замечание: создайте новую ветку вопросов для этой новой проблемы. К сожалению, Stack Overflow не подходит для такого рода ответов в комментариях и задания дополнительных вопросов. - person Konrad Rudolph; 11.03.2010
comment
Хорошо, я начну новую тему с более конкретным вопросом по этому поводу. Сначала я хочу запустить этот код, рассмотрите мою проблему в комментарии к вашему обновленному ответу - person Majgel; 15.03.2010