Статьи

Использование «Natural»: модуль NLP для node.js

Будь то для анализа настроений в Твиттере или для решения поисковых задач, обработка естественного языка (NLP) в последние годы стала точкой опоры для моей хобби. Сначала я обычно полагался наИнструментарий естественного языка (NLTK)которая является богатой библиотекой алгоритмов НЛП дляPython . НЛТК просто фантастический. Это настоящий универсальный магазин НЛП, широко распространенный, хорошо документированный и с открытым исходным кодом. Конечно, я должен был узнать, что делают алгоритмы и как они сочетаются друг с другом, но по большей части тяжелая работа была проделана за меня. Конечно, это была очень продуктивная ситуация!

В прошлом году, однако, принес новую платформу для моего хобби работы: Node.js . Мой узел и его сообщество были молоды, но быстро созревали.

Когда возникла потребность в средствах на естественном языке, и я обнаружил, что выбор довольно тонкий. Я должен быть честным. Это именно то, на что я надеялся; возможность погрузить свои зубы в сами алгоритмы и внести их обратно в молодое, но растущее сообщество.

Таким образом , я начал работу над «естественным» , модулем базовых естественных языков обработки алгоритмов Node.js. Идея была свободно основана на Python NLTK в том, что все алгоритмы находятся в одном пакете. Изначально я не думал, что «естественный» может быть таким же полным, как NLTK, но по мере того, как мое собственное понимание, а также вклад сообщества были подобраны, я стал гораздо более оптимистичным. Кроме того, слияние с node-nltools Роба Эллиса еще в августе 2011 года еще больше укрепило «естественность» за счет быстрого внедрения новых алгоритмов и функций.

Начиная с версии 0.1.5 Роб, другие участники и я смогли собрать следующий список функций:

  • Морфологический

    • Портье
    • Ланкастер
  • фонетический

    • SOUNDEX
    • Metaphone
    • Двойной метафон
  • классификация

    • Наивный байесовский
    • Логистическая регрессия
  • Строка Расстояние

    • Левенштейн (спасибо Сид Наллу)
    • Джаро-Винклер (спасибо Адаму Филлабауму)
    • Коэффициент кости (спасибо Джону Крепецци)
  • лексемизацию

    • Treebank
    • слово
    • Слово-пунктуация
  • сгибание

    • числовой
    • Существительные единственного числа / множественное число
    • Глагол настоящего времени единственного числа / множественное число
  • тс * IDF
  • п-граммы
  • WordNet

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

Установка

Как и большинство узловых модулей, «natural» упакован как NPM и может быть установлен из командной строки следующим образом:

npm install natural

Если вы хотите установить из источника (или внести свой вклад в этом отношении), он может быть найденздесь, на GitHub .

Морфологический

Первый класс алгоритмов, который я хотел бы описать, — это stemming. Стемминг — это процесс приведения слова к корню (не обязательно морфологическому корню). Другими словами, идея состоит в том, чтобы свести все спряжения, времена и формы к одному коренному слову. Этот корень может не выглядеть точно так же, как английский, но должен быть достаточно близок для сравнения.

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

var natural = require('natural'),
stemmer = natural.PorterStemmer;

var stem = stemmer.stem('stems');
console.log(stem);
stem = stemmer.stem('stemming');
console.log(stem);
stem = stemmer.stem('stemmed');
console.log(stem);
stem = stemmer.stem('stem');
console.log(stem);

Выше я просто потребовал основной «естественный» модуль и взял субмодуль PorterStemmer изнутри. Вызов функции «stem» берет произвольную строку и возвращает основу. Приведенный выше код возвращает следующий вывод:

stem
stem
stem
stem

Для удобства стеммеры могут исправлять String с помощью методов, упрощающих процесс, вызываяприкреплятьметод. Строковые объекты будут иметьстебельметод.

stemmer.attach();
stem = 'stemming'.stem();
console.log(stem);

Вполне возможно, что вам будет интересно составить строку, состоящую из многих слов, возможно, из всего документа. прикреплятьметод обеспечиваетtokenizeAndStemметод для достижения этой цели. Он разбивает собственную строку на массив строк, по одной на каждое слово, и объединяет их все. Например:

var stems = 'stems returned'.tokenizeAndStem();
console.log(stems);

производит вывод:

[ 'stem', 'return' ]

Обратите внимание, чтоtokenizeAndStemМетод по умолчанию пропускает определенные слова, которые считаются несоответствующими (стоп-слова) из возвращаемого массива. Чтобы дать указание стеммеру не пропускать стоп-слова, передайте истинноев кtokenizeAndStemдляkeepStopsпараметр. Рассматривать:

console.log('i stemmed words.'.tokenizeAndStem());
console.log('i stemmed words.'.tokenizeAndStem(true));

вывод:

[ 'stem', 'word' ]
[ 'i', 'stem', 'word' ]

Весь приведенный выше код также будет работать со стеймером Lancaster, требуя вместо этого модуль LancasterStemmer, например:

var natural = require('natural'),
    stemmer = natural.LancasterStemmer;

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

Фонетика

Также предоставляются фонетические алгоритмы, чтобы определить, как звучат слова, и сравнить их соответствующим образом. Начиная с версии 0.1.5, старые (и я имею в виду доэлектронные компьютеры старые … как старые 1918 года) SoundEx и более современные алгоритмы Metaphone / Double Metaphone поддерживаются с 0.1.5.

В следующем примере сравниваются строка «фонетика» и преднамеренное неправильное написание «fonetix» и определяется, что они звучат одинаково в соответствии с модулем Metaphone, но тот же шаблон может быть применен к модулям DoubleMetaphone или SoundEx.

var natural = require('natural'),
phonetic = natural.Metaphone;

var wordA = 'phonetics';
var wordB = 'fonetix';

if(phonetic.compare(wordA, wordB))
    console.log('they sound alike!');

Необработанный код, который генерирует фонетический алгоритм, можно получить с помощьюобработатьметод:

var phoneticCode = phonetic.process('phonetics');
console.log(phoneticCode);

в результате чего:

FNTKS

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

phonetic.attach();

if(wordA.soundsLike(wordB))
    console.log('they sound alike!');

прикреплятьтакже патчи вфонетикаиtokenizeAndPhoneticizeметоды извлечения фонетического кода для одного слова и всего корпуса соответственно.

console.log('phonetics'.phonetics());
console.log('phonetics rock'.tokenizeAndPhoneticize());

какие выводы:

FNTKS
[ 'FNTKS', 'RK' ]

Вышесказанное также может использовать SoundEx, заменив следующее на require.

var natural = require('natural'),
    phonetic = natural.SoundEx;

Обратите внимание, что SoundEx и Metaphone могут иметь проблемы с неанглийскими словами, но Double Metaphone должен иметь некоторую степень успеха со многими другими языками.

тс * IDF

Вес tf * idf может использоваться для оценки важности данного слова для данного документа в более широком корпусе (сборник документов). Для веса tf * idf есть два компонента: термин частота и обратная частота документа. Чтобы гарантировать, что часто используемое, хотя и семантически менее важное слово не пользуется большой популярностью, вам нужно убедиться, что в вашем клоне TfIdf много документов.

Рассмотрим следующий код, который добавляет несколько документов в корпус, а затем определяет, насколько важны для них слова «ruby» и «node».

var natural = require('natural'),
    TfIdf = natural.TfIdf,
    tfidf = new TfIdf();

tfidf.addDocument('i code in c.');
tfidf.addDocument('i code in ruby.');
tfidf.addDocument('i code in ruby and node, but node more often.');
tfidf.addDocument('this document is about natural, written in node');
tfidf.addDocument('i code in fortran.');

console.log('node --------------------------------');
tfidf.tfidfs('node', function(i, measure) {
    console.log('document #' + i + ' is ' + measure);
});

console.log('ruby --------------------------------');
tfidf.tfidfs('ruby', function(i, measure) {
    console.log('document #' + i + ' is ' + measure);
});

Предыдущий код выведет веса tf * idf для «узла» и «рубина». Чем выше вес, тем важнее слово для документа.

node --------------------------------
document #0 is 0
document #1 is 0
document #2 is 3.347952867143343
document #3 is 1.6739764335716716
document #4 is 0
ruby --------------------------------
document #0 is 0
document #1 is 1.6739764335716716
document #2 is 1.6739764335716716
document #3 is 0
document #4 is 0

Кроме того, вы можете измерить слово против одного документа.

console.log(tfidf.tfidf('node', 0 /* document index */));
console.log(tfidf.tfidf('node', 1));

Вы также можете получить список всех терминов в документе, упорядоченных по их важности.

tfidf.listTerms(4 /* document index */).forEach(function(item) {
    console.log(item.term + ': ' + item.tfidf);
});

yeilding:

fortran: 1.7047480922384253
code: 1.6486586255873816

сгибание

Основные инфлекторы используются для преобразования существительных между множественным и единичным формами и для преобразования целых чисел в счетчики строк (то есть «1-й», «2-й», «3-й», «4-й» и т. Д.).

В следующем примере слово «радиус» преобразуется во множественное число «радиусы».

var natural = require('natural'),
    nounInflector = new natural.NounInflector();

var plural = nounInflector.pluralize('radius');
console.log(plural);

Сингуляризация следует той же схеме, которая проиллюстрирована в следующем примере, который преобразует слово «пиво» ​​в его единственную форму, «пиво».

var singular = nounInflector.singularize('beers');
console.log(singular);

Так же, как и фонетические модулиприкреплятьпредоставляется метод для исправления String с помощью ярлыков.

nounInflector.attach();
console.log('radius'.pluralizeNoun());
console.log('beers'.singularizeNoun()); 

Экземпляр NounInflector может выполнять пользовательское преобразование, если вы предоставляете выражения черезaddPluralиaddSingularметоды. Поскольку это преобразование не всегда симметрично (иногда для сингулярности форм может потребоваться больше шаблонов, чем для множественного числа), не должно быть взаимно-однозначного отношения междуaddPluralиaddSingularзвонки.

nounInflector.addPlural(/(code|ware)/i, '$1z');
nounInflector.addSingular(/(code|ware)z/i, '$1');

console.log('code'.pluralizeNoun());
console.log('ware'.pluralizeNoun());

console.log('codez'.singularizeNoun());
console.log('warez'.singularizeNoun());

что приведет к:

codez
warez
code
ware

Вот пример использования модуля CountInflector для создания счетчика строк для целых чисел.

var natural = require('natural'),
    countInflector = natural.CountInflector;

console.log(countInflector.nth(1));
console.log(countInflector.nth(2));
console.log(countInflector.nth(3));
console.log(countInflector.nth(4));
console.log(countInflector.nth(10));
console.log(countInflector.nth(11));
console.log(countInflector.nth(12));
console.log(countInflector.nth(13));
console.log(countInflector.nth(100));
console.log(countInflector.nth(101));
console.log(countInflector.nth(102));
console.log(countInflector.nth(103));
console.log(countInflector.nth(110));
console.log(countInflector.nth(111));
console.log(countInflector.nth(112));
console.log(countInflector.nth(113));

производство:

1st
2nd
3rd
4th
10th
11th
12th
13th
100th
101st
102nd
103rd
110th
111th
112th
113th

классификация

Классификация в настоящее время поддерживается алгоритмами Наивного Байеса и Логистической регрессии, хотя естественная реализация Наивного Байеса является наиболее зрелой из двух. Вы можете использовать их для таких задач, как обнаружение спама и анализ настроений.

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

Следующий пример заботится о первом шаге, требуя классификатора и обучая его данным. Естественно, это только образец. Для выполнения каких-либо производственных задач вам понадобится гораздо больше учебных документов (по сотне на класс в зависимости от их размера).

var natural = require('natural'),
classifier = new natural.BayesClassifier();
classifier.addDocument("my unit-tests failed.", 'software');
classifier.addDocument("tried the program, but it was buggy.", 'software');
classifier.addDocument("the drive has a 2TB capacity.", 'hardware');
classifier.addDocument("i need a new power supply.", 'hardware');
classifier.train();

По умолчанию классификатор токенизирует корпус и обрезает его с помощью PorterStemmer. Вы можете использовать LancasterStemmer, передав его конструктору BayesClassifier следующим образом:

var natural = require('natural'),
    stemmer = natural.LancasterStemmer,
    classifier = new natural.BayesClassifier(stemmer);

С подготовленным классификатором он теперь может классифицировать документы черезСортировать пометод:

console.log(classifier.classify('did the tests pass?'));
console.log(classifier.classify('did you buy a new drive?'));

в результате получается:

software
hardware

Точно так же классификатор можно обучать на массивах, а не на строках, минуя токенизацию и основание. Это позволяет потребителю выполнять пользовательскую токенизацию и стемминг, если таковые вообще имеются Это особенно полезно, если корпус не английский.

classifier.addDocument(['unit', 'test'], 'software');
classifier.addDocument(['bug', 'program'], 'software');
classifier.addDocument(['drive', 'capacity'], 'hardware');
classifier.addDocument(['power', 'supply'], 'hardware');

classifier.train();

Можно сохранить и вспомнить результаты тренинга черезспастиметод:

var natural = require('natural'),
classifier = new natural.BayesClassifier();

classifier.addDocument(['unit', 'test'], 'software');
classifier.addDocument(['bug', 'program'], 'software');
classifier.addDocument(['drive', 'capacity'], 'hardware');
classifier.addDocument(['power', 'supply'], 'hardware');

classifier.train();

classifier.save('classifier.json', function(err, classifier) {
    // the classifier is saved to the classifier.json file!
 });

Затем можно было вспомнить тренинг снагрузкаметод:

var natural = require('natural'),
    classifier = new natural.BayesClassifier();

natural.BayesClassifier.load('classifier.json', null, function(err, classifier) {
    console.log(classifier.classify('did the tests pass?'));
});

Обратите внимание, что заменаLogisticRegressionClassifierзаBayesClassifierкак правило, должен работать в качестве замены.

п-граммы

n-граммы — это, по сути, разложение предложения в перекрывающиеся, непрерывные спискиNразмер и полезны для построения вероятностных языковых моделей. В этом случае n-граммы состоят из слов, но вне «естественного» или даже естественного языка обработки они могут быть из других счетных объектов.

Рассмотрим следующие примеры, иллюстрирующие производство триграмм (n-грамм длины 3), биграмм (n-грамм длины 2) и произвольных n-грамм с использованиемтриграммы ,биграммыиНграммы функционируют соответственно.

var NGrams = natural.NGrams;
console.log(NGrams.trigrams('some other words here'));
console.log(NGrams.trigrams(['some',  'other', 'words',  'here']));

оба из которых производят:

[ [ 'some', 'other', 'words' ], [ 'other', 'words', 'here' ] ]    
console.log(NGrams.bigrams('some words here'));
console.log(NGrams.bigrams(['some',  'words',  'here']));

оба из которых производят:

[ [ 'some', 'words' ], [ 'words', 'here' ] ]
console.log(NGrams.ngrams('some other words here for you', 4));

какой вывод:

[ [ 'some', 'other', 'words', 'here' ], [ 'other', 'words', 'here', 'for' ], [ 'words', 'here', 'for', 'you' ] ]

Строка Расстояние

«natural» предоставляет алгоритмы коэффициента Дайса, расстояния Левенштейна и расстояния Яро-Винклера для определения сходства струн. Эти алгоритмы связаны с орфографическим (орфографическим) сходством, а не с фонетикой.

Каждый алгоритм выдает число, указывающее его восприятие сходства, но каждый определяется по-разному и может даже двигаться в противоположных направлениях. Например, чем больше разнородных двух строк, тем больше расстояние Левенштейна, но Джаро-Винклер считает, что две совершенно разнородные строки имеют значение 0 с одинаковыми строками, имеющими значение 1.

В следующем примере показано, как каждый алгоритм воспринимает разницу между словами «исполнение» и «намерение».

var natural = require('natural');

console.log(natural.JaroWinklerDistance('execution', 'intention'));
console.log(natural.LevenshteinDistance('execution', 'intention'));
console.log(natural.DiceCoefficient('execution', 'intention'));

в результате получается:

0.48148148148148145
8
0.375

Теперь рассмотрим полностью идентичные строки.

var natural = require('natural');

console.log(natural.JaroWinklerDistance('same', 'same'));
console.log(natural.LevenshteinDistance('same', 'same'));
console.log(natural.DiceCoefficient('same', 'same'));

что дает:

1
0
1

Заключение и дорожная карта

Ну, это было краткое изложение значительной части «естественного». Многие из алгоритмов имеют дополнительные параметры, которые можно использовать для настройки их работы, и некоторые модули вообще не были представлены, ноофициальный READMEможет помочь заполнить этот пробел.

Там еще много в магазине для «натурального». Хотя нынешний план, безусловно, не ограничивается следующими моментами, к осени 2012 года они действительно должны быть хоть к чему-то привлечены.

  • Неанглийские алгоритмы
  • Чистая версия JavaScript
  • Максимальный энтропийный классификатор
  • Алгоритмы кластеризации (k-means в разработке)
  • Часть речевого тегирования
  • Сегментация предложения Punkt

За исключением k-means, который близок к завершению, я бы хотел, чтобы помощь сообщества была почти у каждого! Чтобы выручить или следовать за выездомрепозиторий GitHub .