machinelearningmastery.ru

Машинное обучение, нейронные сети, искусственный интеллект
Header decor

Home

Как сделать классификацию текста с использованием CNN, TensorFlow и встраивания слов

Дата публикации Jul 6, 2017

Предположим, я дал вам название статьи «Удивительная плоская версия Twitter Bootstrap» и спросил, в какой публикации появилась эта статья: New York Times, TechCrunch или GitHub. Что бы вы догадались? Как насчет статьи под названием «Верховный суд рассмотрит дело по партизанским округам»?

Вы догадались, GitHub и New York Times? Зачем? Такие слова, как Twitter и Major, могут встречаться в любых публикациях, но такие последовательности слов, как Twitter Bootstrap и Верховный суд, чаще встречаются в GitHub и New York Times соответственно. Можем ли мы обучить нейронную сеть этому?

Примечание: Оценщики теперь перешли в ядро ​​Tensorflow.Обновленный код, который использует tf.estimator вместо tf.contrib.learn.estimator, теперь находится на GitHub- использовать обновленный код в качестве отправной точки.

Создание набора данных

Машинное обучение означает учиться на примерах. Чтобы узнать, какая публикация является вероятным источником статьи с ее названием, нам нужно много примеров названий статей вместе с их источником. Хотя он страдает от серьезного смещения отбора (поскольку в него включены только статьи, представляющие интерес для членов команды HN),BigQuery общедоступный набор данных статей Hacker Newsявляется разумным источником этой информации.

query="""
SELECT source, REGEXP_REPLACE(title, '[^a-zA-Z0-9 $.-]', ' ') AS title FROM
(SELECT
ARRAY_REVERSE(SPLIT(REGEXP_EXTRACT(url, '.*://(.[^/]+)/'), '.'))[OFFSET(1)] AS source,
title
FROM
`bigquery-public-data.hacker_news.stories`
WHERE
REGEXP_CONTAINS(REGEXP_EXTRACT(url, '.*://(.[^/]+)/'), '.com$')
AND LENGTH(title) > 10
)
WHERE (source = 'github' OR source = 'nytimes' OR source = 'techcrunch')
"""
traindf = bq.Query(query + " AND MOD(ABS(FARM_FINGERPRINT(title)),4) > 0").execute().result().to_dataframe()
evaldf = bq.Query(query + " AND MOD(ABS(FARM_FINGERPRINT(title)),4) = 0").execute().result().to_dataframe()

По сути, я извлекаю URL и заголовок из набора данных историй Hacker News в BigQuery и разделяю его на набор учебных и оценочных данных (см.Блокнот Datalabдля полного кода). Возможные метки: github, nytimes или techcrunch. Вот как выглядит результирующий набор данных:

Учебный набор данных

Я записал два кадра данных Pandas в файлы CSV (всего 72 000 обучающих примеров, примерно одинаково распределенных между nytimes, github и techcrunch).

Создание словарного запаса

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

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

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

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

lines = ['Some title', 
'A longer title',
'An even longer title',
'This is longer than doc length']

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

Я могу создать словарь, используя следующий код (это не идеально, поскольку словарный процессор хранит все в памяти; для больших наборов данных и более сложной предварительной обработки, такой как включение стоп-слов и нечувствительность к регистру,tf.transformэто лучшее решение - это тема для другого сообщения в блоге):

import tensorflow as tf
from tensorflow.contrib import lookup
from tensorflow.python.platform import gfile

MAX_DOCUMENT_LENGTH = 5
PADWORD = 'ZYXW'

# create vocabulary
vocab_processor = tf.contrib.learn.preprocessing.VocabularyProcessor(MAX_DOCUMENT_LENGTH)
vocab_processor.fit(lines)
with gfile.Open('vocab.tsv', 'wb') as f:
f.write("{}\n".format(PADWORD))
for word, index in vocab_processor.vocabulary_._mapping.iteritems():
f.write("{}\n".format(word))
N_WORDS = len(vocab_processor.vocabulary_)

В приведенном выше коде я дополню короткие заголовки ПАРОЛЕМ, которое, как я ожидаю, никогда не встречается в реальном тексте. Названия будут дополнены или усечены до 5 слов. Я передаю набор учебных данных («линии» в приведенном выше примере), а затем записываю полученный словарный запас. Словарный запас оказывается:

ZYXW
A
even
longer
title
This
doc
is
Some
An
length
than
<UNK>

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

Учитывая вышеупомянутый словарь, мы можем преобразовать любой заголовок в набор чисел:

table = lookup.index_table_from_file(
vocabulary_file='vocab.tsv', num_oov_buckets=1, vocab_size=None, default_value=-1)
numbers = table.lookup(tf.constant('Some title'.split()))
with tf.Session() as sess:
tf.tables_initializer().run()
print "{} --> {}".format(lines[0], numbers.eval())

Приведенный выше код будет искать слова «Some» и «title» и возвращать индексы [8, 4] на основе словарного запаса. Конечно, в фактическом графике обучения / прогнозирования нам также необходимо убедиться в том, что он дополнен / усечен. Давайте посмотрим, как это сделать дальше.

Обработка текста

Сначала мы начинаем со строк (каждая строка является заголовком) и разбиваем заголовки на слова:

# string operations
titles = tf.constant(lines)
words = tf.string_split(titles)

Это приводит к:

titles= ['Some title' 'A longer title' 'An even longer title'
'This is longer than doc length']
words= SparseTensorValue(indices=array([[0, 0],
[0, 1],
[1, 0],
[1, 1],
[1, 2],
[2, 0],
[2, 1],
[2, 2],
[2, 3],
[3, 0],
[3, 1],
[3, 2],
[3, 3],
[3, 4],
[3, 5]]), values=array(['Some', 'title', 'A', 'longer', 'title', 'An', 'even', 'longer',
'title', 'This', 'is', 'longer', 'than', 'doc', 'length'], dtype=object), dense_shape=array([4, 6]))

Функция string_split () в TensorFlow в итоге создает SparseTensor. Разговор об излишне полезном API. Я не хочу, чтобы это автоматически создавало отображение, поэтому я преобразую разреженный тензор в плотный и затем найду индекс из моего собственного словаря:

# string operations
titles = tf.constant(lines)
words = tf.string_split(titles)
densewords = tf.sparse_tensor_to_dense(words, default_value=PADWORD)
numbers = table.lookup(densewords)

Теперь плотные слова и числа соответствуют ожидаемым (обратите внимание на заполнение ПАРОЛЕМ:

dense= [['Some' 'title' 'ZYXW' 'ZYXW' 'ZYXW' 'ZYXW']
['A' 'longer' 'title' 'ZYXW' 'ZYXW' 'ZYXW']
['An' 'even' 'longer' 'title' 'ZYXW' 'ZYXW']
['This' 'is' 'longer' 'than' 'doc' 'length']]
numbers= [[ 8 4 0 0 0 0]
[ 1 3 4 0 0 0]
[ 9 2 3 4 0 0]
[ 5 7 3 11 6 10]]

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

padding = tf.constant([[0,0],[0,MAX_DOCUMENT_LENGTH]])
padded = tf.pad(numbers, padding)
sliced = tf.slice(padded, [0,0], [-1, MAX_DOCUMENT_LENGTH])

Это создает матрицу пакетного размера x 5, где более короткие заголовки дополняются нулем:

padding= [[0 0]
[0 5]]
padded= [[ 8 4 0 0 0 0 0 0 0 0 0]
[ 1 3 4 0 0 0 0 0 0 0 0]
[ 9 2 3 4 0 0 0 0 0 0 0]
[ 5 7 3 11 6 10 0 0 0 0 0]]
sliced= [[ 8 4 0 0 0]
[ 1 3 4 0 0]
[ 9 2 3 4 0]
[ 5 7 3 11 6]]

Я использовал MAX_DOCUMENT_LENGTH из 5 в приведенных выше примерах, чтобы показать вам, что происходит. В реальном наборе данных заголовки длиннее 5 слов. Итак, я буду использовать

MAX_DOCUMENT_LENGTH = 20

Форманарезанныхматрица будет иметь размер пакета x MAX_DOCUMENT_LENGTH, то есть размер пакета x 20.

Вложение

Теперь, когда наши слова были заменены числами, мы могли бы просто выполнить горячее кодирование, но это привело бы к чрезвычайно широкому вводу - в наборе данных заголовков есть тысячи уникальных слов. Лучшим подходом является уменьшение размерности входных данных - это делается через слой внедрения (см.полный кодВот):

EMBEDDING_SIZE = 10
embeds = tf.contrib.layers.embed_sequence(sliced,
vocab_size=N_WORDS, embed_dim=EMBEDDING_SIZE)

Как только у нас есть вложение, у нас теперь есть представление для каждого слова в заголовке. Результатом встраивания является тензор размера пакета x MAX_DOCUMENT_LENGTH x EMBEDDING_SIZE, потому что заголовок состоит из слов MAX_DOCUMENT_LENGTH, и каждое слово теперь представлено числами EMBEDDING_SIZE. (Привыкайте вычислять тензорные формы на каждом шаге вашего кода TensorFlow - это поможет вам понять, что делает код и что означают измерения).

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

свертка

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

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

WINDOW_SIZE = EMBEDDING_SIZE
STRIDE = int(WINDOW_SIZE/2)
conv = tf.contrib.layers.conv2d(embeds, 1, WINDOW_SIZE,
stride=STRIDE, padding='SAME') # (?, 4, 1)
conv = tf.nn.relu(conv) # (?, 4, 1)
words = tf.squeeze(conv, [2]) # (?, 4)

Напомним, что результатом встраивания является тензор 20 x 10 (пока не будем обращать внимание на размер пакета; все операции здесь выполняются с одним заголовком за раз). Теперь я применяю взвешенное среднее в окне 10x10 к встроенному представлению заголовка, перемещая окно на 5 слов (STRIDE = 5) и применяя его снова. Итак, у меня будет 4 таких результата свертки. Затем я применяю нелинейное преобразование (relu) к результатам свертки.

У меня сейчас четыре результата. Я могу просто связать их через плотный слой с выходным слоем:

n_classes = len(TARGETS)     
logits = tf.contrib.layers.fully_connected(words, n_classes,
activation_fn=None)

Если вы привыкли к моделям изображений, вы можете быть удивлены тем, что я использовал сверточный слой, но не слой maxpool. Причиной использования слоя maxpool является добавление пространственной инвариантности к сети - интуитивно говоря, вы хотите найти кошку независимо от того, где на изображении находится кошка. Тем не менее, пространственное расположение в названии довольно важно. Вполне возможно, что заголовки статей в New York Times, как правило, начинаются с разных слов, чем слова на GitHub. Следовательно, я не использовал слой maxpool для этой задачи.

Учитывая логиты, мы можем выяснить источник, фактически выполнив ЗАДАЧИ [max (logits)]. В TensorFlow это делается с помощью tf.gather:

predictions_dict = {      
'source': tf.gather(TARGETS, tf.argmax(logits, 1)),
'class': tf.argmax(logits, 1),
'prob': tf.nn.softmax(logits)
}

Просто чтобы быть полным, я также отправляю фактический индекс класса и вероятности каждого класса.

Обучение и развертывание

С кодом все написано (см.полный код здесь), Я могу обучить его на Cloud ML Engine:

OUTDIR=gs://${BUCKET}/txtcls1/trained_model
JOBNAME=txtcls_$(date -u +%y%m%d_%H%M%S)
echo $OUTDIR $REGION $JOBNAME
gsutil -m rm -rf $OUTDIR
gsutil cp txtcls1/trainer/*.py $OUTDIR
gcloud ml-engine jobs submit training $JOBNAME \
--region=$REGION \
--module-name=trainer.task \
--package-path=$(pwd)/txtcls1/trainer \
--job-dir=$OUTDIR \
--staging-bucket=gs://$BUCKET \
--scale-tier=BASIC --runtime-version=1.2 \
-- \
--bucket=${BUCKET} \
--output_dir=${OUTDIR} \
--train_steps=36000

Набор данных довольно мал, поэтому обучение закончилось менее чем за пять минут, и я получил точность оценочного набора данных в 73%.

Затем я могу развернуть модель как микросервис в Cloud ML Engine:

MODEL_NAME="txtcls"
MODEL_VERSION="v1"
MODEL_LOCATION=$(gsutil ls \
gs://${BUCKET}/txtcls1/trained_model/export/Servo/ | tail -1)
gcloud ml-engine models create ${MODEL_NAME} --regions $REGION
gcloud ml-engine versions create ${MODEL_VERSION} --model \
${MODEL_NAME} --origin ${MODEL_LOCATION}

прогнозирование

Чтобы получить модель для прогнозирования, мы можем отправить ей запрос JSON:

from googleapiclient import discovery
from oauth2client.client import GoogleCredentials
import json

credentials = GoogleCredentials.get_application_default()
api = discovery.build('ml', 'v1beta1', credentials=credentials,
discoveryServiceUrl='https://storage.googleapis.com/cloud-ml/discovery/ml_v1beta1_discovery.json')

request_data = {'instances':
[
{
'title': 'Supreme Court to Hear Major Case on Partisan Districts'
},
{
'title': 'Furan -- build and push Docker images from GitHub to target'
},
{
'title': 'Time Warner will spend $100M on Snapchat original shows and ads'
},
]
}

parent = 'projects/%s/models/%s/versions/%s' % (PROJECT, 'txtcls', 'v1')
response = api.projects().predict(body=request_data, name=parent).execute()
print "response={0}".format(response)

Это приводит к ответу JSON:

response={u'predictions': [{u'source': u'nytimes', u'prob': [0.7775614857673645, 5.86951500736177e-05, 0.22237983345985413], u'class': 0}, {u'source': u'github', u'prob': [0.1087314561009407, 0.8909648656845093, 0.0003036781563423574], u'class': 1}, {u'source': u'techcrunch', u'prob': [0.0021869686897844076, 1.563105769264439e-07, 0.9978128671646118], u'class': 2}]}

Обученная модель предсказывает, что статья Верховного суда с вероятностью 78% поступит из New York Times. Статья Docker с вероятностью 89% от GitHub, согласно сервису, а статья Time Warner с вероятностью 100% от TechCrunch. Это 3/3.

Ресурсы:Весь код на GitHub здесь:https://github.com/GoogleCloudPlatform/training-data-analyst/tree/master/blogs/textclassification

Оригинальная статья

Footer decor

© machinelearningmastery.ru | Ссылки на оригиналы и авторов сохранены. | map