Как я уже упоминал в первой части этой серии из трех частей, Capital Finder имеет множество полезных приложений. Во второй части я подробно обсудил, откуда мы получаем различные точки данных и как мы превращаем эти данные в единое и интуитивно понятное представление об альтернативных финансах в развивающихся странах. В этой заключительной части серии я провожу читателей по этапам - и даже по части кода, - которые делают все это возможным.

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

Чтобы самостоятельно опробовать примеры кода, вам понадобится ваш собственный набор данных для обработки. Вы можете найти наборы данных в Интернете, чтобы с ними попрактиковаться; например, Репозиторий машинного обучения UCI или пакет Python NLTK, который также предоставляет легкодоступные данные, с которыми вы можете работать.

Сбор данных об альтернативных финансах

Первый шаг в любом конвейере данных - это сбор данных. В AlliedCrowds мы создали проприетарный веб-скребок, который использует библиотеку asyncio python для выполнения одновременных HTTP-запросов. Это позволяет нам масштабироваться до сотен тысяч веб-страниц в день на стандартной машине.

Асинхронная функциональность важна, если вам нужно масштабировать процесс, связанный с вводом / выводом. При сканировании в Интернете процесс зависит от скорости сети и времени отклика вашего целевого веб-сайта. Среднее время, необходимое для загрузки некэшированного веб-сайта, составляет примерно 5 секунд. Излишне говорить, что 5 секунд на запрос недопустимы, учитывая, что мы будем делать миллионы таких запросов (например, 5 миллионов секунд это ~ 58 дней).

Вместо того, чтобы делать один запрос к одному серверу и ждать ответа, мы делаем столько запросов, сколько наш ЦП может обработать, а наша ОЗУ может удерживать в памяти. Это означает, что мы больше не связаны медленными веб-сайтами. Другими словами, мы решили проблему привязки ввода / вывода.

Одно предостережение: с таким мощным веб-скребком легко перегрузить сервер веб-сайта, который вы сканируете. Чтобы проявить уважение, лучше всего оптимизировать очередь запросов так, чтобы вы ориентировались на несколько веб-сайтов одновременно. Например, если вы сканируете 60 страниц в секунду, они будут на 60 разных сайтах. Это также помогает ускорить процесс, потому что веб-сайты, как правило, замедляются из-за того, что они перегружены трафиком.

Хранение данных

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

Для остальной части этого руководства будет достаточно локальной файловой системы.

Данные очистки

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

HTML-код удивительно много рассказывает о рассматриваемом веб-сайте. Например, HTML содержит информацию о:

  • На каком языке написан сайт
  • Где основан сайт
  • Какие изображения появляются на странице
  • О чем сайт
  • Что такое заголовок конкретной страницы
  • Какое название у конкретной статьи
  • Что такое подзаголовок этой статьи
  • Какие слова / фразы выделены жирным шрифтом
  • На какие еще темы ссылается эта тема

Вы можете быть удивлены тем, насколько HTML добавляет глубину вашему пониманию веб-страницы. HTML формирует основу того, как веб-сайты оптимизируются для поисковых систем и, следовательно, классифицируются во всей сети. Мы ранжируем эти различные аспекты HTML в соответствии с нашим взглядом на их важность. Однако для этого урока мы просто объединим весь видимый текст и будем рассматривать его как равный.

Это подводит нас к нашему первому фрагменту кода. В нашем случае мы используем библиотеку BeautifulSoup для извлечения всего видимого текста из нашего необработанного HTML:

from bs4 import BeautifulSoup
from bs4.element import Comment
    def tag_visible(element):
        if element.parent.name in ['style', 'script', 'head', 'title', 'meta', '[document]']:
            return False
        if isinstance(element, Comment):
            return False
        return True

    def text_from_html(soup):
        texts = soup.findAll(text=True)
        visible_texts = filter(tag_visible, texts)
        return u" ".join(t.strip() for t in visible_texts)

Код довольно понятен. Метод text_from_html использует встроенный в Beautiful Soup метод findAll для поиска всего текста, а затем использует метод tag_visible для фильтрации любого содержимого, которое не должны быть прочитаны посетителем сайта.

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

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

Создание наборов данных для обучения, тестирования и проверки

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

Один из наиболее простых вариантов использования НЛП - это классификация поставщиков капитала по типу финансирования, и мы обнаружили, что это значительно более эффективно, чем наши платные аналитики, сравнивая количество ложноположительных и ложноотрицательных результатов с известными данными.

Таблица 1: выше сравниваются ложноположительные и ложноотрицательные результаты в данных категоризации AlliedCrowds по странам. В нашей базе данных есть сотни тысяч пар стран и поставщиков капитала, и эта задача оказалась слишком обременительной для наших аналитиков. Наши результаты значительно улучшились, когда мы перешли на классификатор естественного языка.

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

Мы собрали наш обучающий набор путем кропотливого процесса просмотра и проверки подмножества 6000 наших поставщиков капитала до тех пор, пока мы не были уверены, что он был на 100% точным.

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

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

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

В заключение я кратко коснусь данных валидации. Данные проверки используются для настройки гиперпараметров (которые выходят за рамки этого блога). Гиперпараметры, как следует из названия, представляют собой своего рода мета-параметр, который может помочь точно настроить вашу модель. Одна из проблем, возникающих в ходе цикла обучения / тестирования, - это переоснащение гиперпараметров. Набор данных проверки гарантирует, что вы не переобучаете эти гиперпараметры, так же как данные тестирования гарантируют, что вы не переобоите параметры своей модели.

Обработка естественного языка (NLP)

Когда у нас есть наборы данных для обучения и тестирования, мы готовы обучать и тестировать наш классификатор естественного языка. Чтобы провести вас через это руководство, мы будем использовать упомянутый выше пример классификации поставщиков капитала по типу поставщика. Мы упростим ситуацию, сравнив микрофинансовые организации (МФО) с фирмами венчурного капитала (ВК).

Говоря человеческими словами, мы собираемся показать нашему классификатору набор текста веб-сайтов с веб-сайтов МФО и кучу текста с веб-сайтов венчурных капиталистов, чтобы позволить ему «выяснить» разницу. Полное объяснение того, что происходит под капотом, выходит за рамки этого руководства, но классификатор, по сути, использует серию предположений и проверок для определения основных различий.

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

Мы будем использовать инструменты, предоставляемые библиотекой gensim, а также набором инструментов для естественного языка (NLTK). Вам не нужно знать эти библиотеки, чтобы следовать, я объясню каждый из используемых методов.

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

import gensim
num_rows = 50
def build_texts():
    for subdirectory in os.listdir(f"/Directory/"):
        with open(f"/Directory/{subdirectory}/text.txt") as f:
            doc = " ".join([x.strip() for x in islice(f, num_rows)])
            yield gensim.utils.simple_preprocess(doc, deacc=True, min_len=3)      
print("Text generator initialized...")

В этом примере Каталог - это каталог, в котором хранится ваш текст. Мы перебираем каждый подкаталог в этом каталоге, который должен соответствовать объекту, который вы хотите классифицировать. Здесь следует отметить, что мы берем первые 50 строк текста из каждого файла, чтобы немного ускорить обработку. В производстве вы захотите использовать все текстовые данные.

Предварительная обработка

Хотя у нас есть необработанный текст, есть еще немного предварительной обработки, которую мы хотели бы сделать перед обучением нашего классификатора.

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

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

train_texts = list(build_texts())
bigram_phrases = gensim.models.Phrases(train_texts, common_terms=stops)
bigram = gensim.models.phrases.Phraser(bigram_phrases)

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

assert (bigram['microfinance', 'institution'][0] == "microfinance_institution")
assert (bigram['venture', 'capital'][0] == "venture_capital")
assert (bigram['solar', 'panel'][0] == "solar_panel")
assert (bigram['big', 'data'][0] == "big_data")
assert (bigram['united', 'states'][0] == "united_states")

Теперь, когда у нас есть обученные биграммы, мы собираемся создать словарь для хранения имени поставщика, типа поставщика и текста. В этом примере для простоты мы сохраняем текстовый файл с именем provider_type.txt с именем типа поставщика в том же каталоге, что и text.txt.

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

import nltk
from gensim.parsing.preprocessing import STOPWORDS
stops = set(stopwords.words('english')).union(set(STOPWORDS))
documents = []
    for subdirectory in os.listdir(f"/Directory/"):
        with open(f"/Directory/{subdirectory}/text.txt") as f:
            with open(f"/Directory/{subdirectory}/provider_type.txt") as pt:
                provider_type = pt.readline()
            provider = dict(name=subdirectory, provider_type=provider_type)
            text = " ".join([x.strip() for x in islice(f, num_rows)])
            tokens = [word.lower() for word in nltk.word_tokenize(text) if word.isalpha() and word.lower() not in stops]
            tokens = bigram[tokens]
            provider['tokens'] = tokens
            documents.append(provider)

Сначала мы используем метод word_tokenize из nltk, который разбивает строку текста на слова и знаки препинания.

Мы также используем метод isalpha, который проверяет, состоит ли слово только из буквенных символов, потому что это все, что нас интересует.

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

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

Наборы функций

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

Для наших функций мы решили использовать 100 наиболее распространенных слов, исключая любые слова из трех или менее символов, во всех документах. Мы используем класс FreqDist nltk, чтобы идентифицировать их, и вручную определили две дополнительные функции: «venture» и «micro». Это будет особенно полезно для различения МФО и венчурных фирм.

custom_word_features = ['venture', 'micro']
num_features = 100
all_words = nltk.FreqDist(word.lower() for provider in documents for word in provider['tokens'])
word_features = [word for (word, freq) in all_words.most_common(num_features) if len(word) > 3]
word_features = word_features + custom_word_features

Мы создаем метод под названием document_features, который возвращает функции для данного документа.

def document_features(document):
    document_words = set(document['tokens'])
    features = {}
    for word_feature in word_features:
        contains_feature = False
        for document_word in document_words:
            if word_feature in document_word:
                features[f"contains({document_word})"] = True
                contains_feature = True               
        features[f"contains({word_feature})"] = contains_feature
  return features

Вышеупомянутый метод может показаться запутанным из-за вложенных циклов for, но на самом деле он довольно прост. В нем мы определяем, содержатся ли признаки (сегменты слов), которые мы идентифицировали, в токенах анализируемого документа. Если да, то мы добавляем эту функцию в список функций в документе, если нет, мы исключаем ее как функцию. Когда у нас есть список функций для каждого документа, мы можем сравнить эти функции, чтобы определить, какие функции наиболее подходят для каждого типа провайдера (MFI или VC).

Обучение нашего классификатора

Наконец, мы можем обучить наш классификатор!

random.shuffle(documents)
featuresets = [(document_features(d), provider_type) for d in documents if d[‘provider_type’] in provider_types]
num_featuresets = len(featuresets)
train_set, test_set = featuresets[:math.floor(num_featuresets/2)], featuresets[math.floor(num_featuresets/2):]
classifier = nltk.NaiveBayesClassifier.train(train_set)

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

Затем мы используем описанный выше метод document_features для создания наших наборов функций, которые представляют собой кортеж функций документа и типа поставщика.

Затем мы разделим наши тестовые наборы поровну и запустим classifier = nltk.NaiveBayesClassifier.train (train_set). Существует множество различных реализаций классификаторов естественного языка, но для наших целей NaiveBayesClassifier, предоставляемый nltk, отлично работает, и его очень легко реализовать.

Трудно понять, что 62 строки кода в этом примере предназначены для очистки, обработки и предварительной обработки данных и что только 1 строка используется для фактического обучения классификатора, но такова природа науки о данных.

А теперь пора протестировать наш новый классификатор на наших тестовых данных:

print("Classifier accuracy percent:", (nltk.classify.accuracy(classifier, test_set))*100)

Это говорит нам о точности нашего классификатора применительно к нашим тестовым данным. Пример вывода, который мы получаем, когда запускаем наш:

Classifier accuracy percent: 91.28367670364501

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

Один из примеров - показать наиболее информативные функции:

classifier.show_most_informative_features(150)

Это покажет нам 150 наиболее важных аспектов, которые наша модель использовала для определения своих классификаций. Они должны пройти «тест на глаз». Например, мы ожидаем, что все, что содержит слова «микро» или «предприятие», будет чрезвычайно информативным. Пример вывода:

Most Informative Features
contains(savings_account) = True           Microf : Ventur =     67.5 : 1.0
contains(account_opening) = True           Microf : Ventur =     50.1 : 1.0
 contains(savings_loans) = True           Microf : Ventur =     42.5 : 1.0
  contains(micro_credit) = True           Microf : Ventur =     38.8 : 1.0
contains(microfinance_bank) = True           Microf : Ventur =     38.0 : 1.0
contains(personal_loans) = True           Microf : Ventur =     35.0 : 1.0
      contains(startups) = True           Ventur : Microf =     31.4 : 1.0
   contains(marketplace) = True           Ventur : Microf =     31.1 : 1.0
contains(savings_products) = True           Microf : Ventur =     30.1 : 1.0
contains(loan_repayments) = True           Microf : Ventur =     27.4 : 1.0
contains(portfolio_companies) = True           Ventur : Microf =     26.8 : 1.0
contains(loan_application) = True           Microf : Ventur =     24.4 : 1.0
 contains(asset_finance) = True           Microf : Ventur =     23.6 : 1.0
contains(micro_businesses) = True           Microf : Ventur =     23.6 : 1.0
contains(savings_accounts) = True           Microf : Ventur =     23.3 : 1.0
...

Этот пример действительно проходит тест на глаз, каждая функция соответствует правильной категории.

содержит (save_account) = True Microf: Ventur = 67,5: 1,0

Сообщает нам, что если в тексте содержится «сберегательный счет», то вероятность того, что это микрофинансовая организация, в 67,5 раз выше, чем компания венчурного капитала. Довольно круто, но мы не поняли, почему наш классификатор точен только на 91%.

Пример немного более глубокого анализа, который вы можете выполнить, - это следующий, который печатает имя поставщика, тип поставщика и первые 50 токенов. Благодаря этому анализу мы фактически обнаружили, что классификатор верен, а данные наших тестов неверны!

for d in documents:
      featureset = (document_features(d), provider_type) 
            if classifier.classify(featureset[0]) == featureset[1]:
                continue
            else:
                print("******")
                print(f"{d['name']}")
                print(f"{d['provider_type']}")
                print(f"{d['tokens'][:50]}")

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

***INCORRECT!***
iSGS Investment Works
['Venture Capital']
['value', 'team', 'company', 'access', '五嶋一人', 'kazuhito', 'goshima', '代表取締役', '代表パートナー', '代表取締役', '佐藤真希子', 'makiko', 'sato', '取締役', '代表パートナー', '新卒一期生', 'mvp', '取締役', '代表パートナーに就任', '菅原敬', 'kei', 'sugawara', '取締役', '代表パートナー', '現アクセンチュア', 'ジャパン', 'investor', 'が発表した', 'executive', 'team', 'のインターネットセクター', 'best', 'cfo部門', '取締役', 'company', '社名', '株式会社isgs', 'インベストメントワークス', 'isgs', 'investment', 'works', '所在地', '資本金', '取締役_代表取締役', '代表パートナー', '五嶋一人', '取締役', '代表パートナー', '佐藤真希子', '取締役']
***INCORRECT!***
N2V
['Venture Capital']
['من', 'نحن', 'الوظائف', 'اتصل_بنا_english_العودة', 'إطلاق', 'مبادرة', 'تك', 'سباركس', 'نحن_فخورون', 'بأن', 'نعلن', 'لكم', 'عن', 'إطلاق', 'آخر', 'مبادراتنا', 'لدعم_الشركات', 'الناشئة', 'والرياديين', 'في', 'المنطقة', 'وهي', 'مبادرة', 'تك', 'سباركس', 'تهدف', 'المبادرة', 'لمساعدة', 'الرياديين', 'لبناء', 'مشاريعهم', 'والإرتقاء', 'بها', 'من', 'خلال', 'عرض', 'مقابلات', 'ملهمة', 'مع', 'شخصيات', 'بارزة', 'في', 'عالم', 'الريادة', 'من', 'موجهين', 'وأكثر', 'الهدف', 'الرئيسي', 'لمبادرة']
***INCORRECT!***
Al Tamimi Investments
['Venture Capital']
['javascript_required', 'enable_javascript', 'allowed', 'page']

Если ваш классификатор работает не так, как вы ожидаете, посмотрите, какие примеры он дает, и посмотрите, есть ли какие-то общие тенденции!

Заключение

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