...

понедельник, 9 декабря 2013 г.

Введение в анализ текстовой информации с помощью Python и методов машинного обучения

Введение




Сегодня я продолжу рассказ о применении методов анализа данных и машинного обучения на практических примерах. В прошлой статье мы с вами разбирались с задачей кредитного скоринга. Ниже я попытаюсь продемонстрировать решение другой задачи с того же турнира, а именно «Задачи о паспортах» (Задание №2).

При решении будут показаны основы анализа текстовой информации, а также ее кодирование для построения модели с помощью Python и модулей для анализа данных (pandas, scikit-learn, pymorphy).



Постановка задачи




При работе с большим объёмом данных важно поддерживать их чистоту. А при заполнении заявки на банковский продукт необходимо указывать полные паспортные данные, в том числе и поле «кем выдан паспорт», число различных вариантов написаний одного и того же отделения потенциальными клиентами может достигать нескольких сотен. Важно понимать, не ошибся ли клиент, заполняя другие поля: «код подразделения», «серию/номер паспорта». Для этого необходимо сверять «код подразделения» и «кем выдан паспорт».

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

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




Загрузим данные и посмотрим, что мы имеем:

from pandas import read_csv
import pymorphy2
from sklearn.feature_extraction.text import HashingVectorizer
from sklearn.cross_validation import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, roc_auc_score
from sklearn.decomposition import PCA

train = read_csv('https://static.tcsbank.ru/documents/olymp/passport_training_set.csv',';', index_col='id' ,encoding='cp1251')
train.head(5)















































passport_div_codepassport_issuer_namepassport_issue_month/year
id
1422008БЕЛОВСКИМ УВД КЕМЕРОВСКОЙ ОБЛАСТИ11M2001
2500112ТП №2 В ГОР. ОРЕХОВО-ЗУЕВО ОУФМС РОССИИ ПО МО ...03M2009
3642001ВОЛЖСКИМ РОВД ГОР.САРАТОВА04M2002
4162004УВД МОСКОВСКОГО РАЙОНА Г.КАЗАНЬ12M2002
580001ОТДЕЛОМ ОФМС РОССИИ ПО РЕСП КАЛМЫКИЯ В Г ЭЛИСТА08M2009



Теперь можно посмотреть как пользователи записывают поле «кем выдан паспорт» на примере какого-либо подразделения:

example_code = train.passport_div_code[train.passport_div_code.duplicated()].values[0]
for i in train.passport_issuer_name[train.passport_div_code == example_code].drop_duplicates():
print i




ОТДЕЛЕНИЕМ УФМС РОССИИ ПО РЕСПУБЛИКЕ КАРЕЛИЯ В МЕДВЕЖ. Р-Е

ОТДЕЛЕНИЕМ УФМС РОССИИ ПО Р. КАРЕЛИЯ В МЕДВЕЖЬЕГОРСКОМ РАЙОНЕ

ОТДЕЛЕНИЕМ УФМС РОССИИ ПО РЕСП КАРЕЛИЯ В МЕДВЕЖЬЕГОРСКОМ Р-НЕ

ОТДЕЛЕНИЕМ УФМС РОССИИ ПО РЕСПУБЛИКЕ КАРЕЛИЯ В МЕДВЕЖЬЕГОРСКОМ РАЙОНЕ

ОУФМС РОССИИ ПО РЕСПУБЛИКЕ КАРЕЛИЯ В МЕДВЕЖЬЕГОРСКОМ РАЙОНЕ

УФМС РОССИИ ПО РК В МЕДВЕЖЬЕГОРСКОМ РАЙОНЕ

ОТДЕЛЕНИЕМ УФМС РОССИИ ПО РЕСПУБЛИКЕ КАРЕЛИЯ МЕДВЕЖЬЕГОРСКОМ Р-ОНЕ

ОТДЕЛЕНИЕМ УФМС РОССИИ ПО РК В МЕДВЕЖЬЕГОРСКОМ РАЙОНЕ

ОТДЕЛЕНИЕМ УФМС РОССИИ ПО РЕСПУБЛИКЕ КОРЕЛИЯ В МЕДВЕЖИГОРСКОМ РАЙОНЕ

УФМС РОССИИ ПО Р. КАРЕЛИЯ МЕДВЕЖЬЕГОРСКОГО Р-НА

ОТДЕЛОМ УФМС РОССИИ ПО РЕСПУБЛИКЕ КАРЕЛИЯ В МЕДВЕЖЬЕГОРСКОМ

УФМС РЕСПУБЛИКИ КАРЕЛИИ МЕДВЕЖЬЕГОРСКОГО Р-ОН

МЕДВЕЖЬЕГОРСКИМ ОВД

Как можно заметить нужно на поле действительно заполняется криво. Но для нормально кодирования мы должны привести это поле к более-менее нормальному (однозначному) виду.

Для начала я бы предложил привести все записи к одному регистру, например, чтобы все буквы стали строчными. Это легко сделать с помощью атрибута str, столбца DataFrame'a. Этот атрибут позволяет работать со столбцом как с строкой, а также выполнять различного рода поиск и замену по регулярным выражениям:



train.passport_issuer_name = train.passport_issuer_name.str.lower()
train[train.passport_div_code == example_code].head(5)















































passport_div_codepassport_issuer_namepassport_issue_month/year
id
19100010отделением уфмс россии по республике карелия в...04M2008
22100010отделением уфмс россии по р. карелия в медвежь...10M2009
5642100010отделением уфмс россии по респ карелия в медве...08M2008
6668100010отделением уфмс россии по республике карелия в...08M2011
8732100010отделением уфмс россии по республике карелия в...08M2012



C регистром определились. Далее надо по возможности избавиться от популярных сокращений, например район, город и т.д. Сделаем это с помощью регулярных выражений. Pandas предоставляет удобное использование регулярных выражений применительно к каждому столбцу. Это выглядит так:

train.passport_issuer_name = train.passport_issuer_name.str.replace(u'р-(а|й|о|н|е)*',u'район')
train.passport_issuer_name = train.passport_issuer_name.str.replace(u' г( |\.|(ор(\.| )))', u' город ')
train.passport_issuer_name = train.passport_issuer_name.str.replace(u' р(\.|есп )', u' республика ')
train.passport_issuer_name = train.passport_issuer_name.str.replace(u' адм([а-я]*)(\.)?', u' административный ')
train.passport_issuer_name = train.passport_issuer_name.str.replace(u' окр(\.| |уга( )?)', u' округ ')
train.passport_issuer_name = train.passport_issuer_name.str.replace(u' ао ', u' административный округ ')




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

train.passport_issuer_name = train.passport_issuer_name.str.replace(u' - ?', u'-')
train.passport_issuer_name = train.passport_issuer_name.str.replace(u'[^а-я -]','')
train.passport_issuer_name = train.passport_issuer_name.str.replace(u'- ',' ')
train.passport_issuer_name = train.passport_issuer_name.str.replace(u' *',' ')




На следующем шаге, надо расшифровать аббревиатуры, типа УВД, УФНС, ЦАО, ВАО и т.д., т.к. этих их в принципе не много, но на качестве дальнейшего кодирования это скажется положительно. Например если у нас будет две записи «УВД» и «управление внутренних дел», то закодированы они будут по разному, т. к. для компьютера это разные значения.

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

sokr = {u'нао': u'ненецкий автономный округ',
u'хмао': u'ханты-мансийский автономный округ',
u'чао': u'чукотский автономный округ',
u'янао': u'ямало-ненецкий автономный округ',
u'вао': u'восточный административный округ',
u'цао': u'центральный административный округ',
u'зао': u'западный административный округ',
u'cао': u'северный административный округ',
u'юао': u'южный административный округ',
u'юзао': u'юго-западный округ',
u'ювао': u'юго-восточный округ',
u'свао': u'северо-восточный округ',
u'сзао': u'северо-западный округ',
u'оуфмс': u'отдел управление федеральной миграционной службы',
u'офмс': u'отдел федеральной миграционной службы',
u'уфмс': u'управление федеральной миграционной службы',
u'увд': u'управление внутренних дел',
u'ровд': u'районный отдел внутренних дел',
u'говд': u'городской отдел внутренних дел',
u'рувд': u'районное управление внутренних дел',
u'овд': u'отдел внутренних дел',
u'оувд': u'отдел управления внутренних дел',
u'мро': u'межрайонный отдел',
u'пс': u'паспортный стол',
u'тп': u'территориальный пункт'}


Теперь, собственно произведем расшифровку абривеатур и отформатируем полученные записи:



for i in sokr.iterkeys():
train.passport_issuer_name = train.passport_issuer_name.str.replace(u'( %s )|(^%s)|(%s$)' % (i,i,i), u' %s ' % (sokr[i]))

#удалим лишние пробелы в конце и начале строки
train.passport_issuer_name = train.passport_issuer_name.str.lstrip()
train.passport_issuer_name = train.passport_issuer_name.str.rstrip()




Предварительный этап обработки поля «кем выдан паспорт» на этом закончим. И перейдем к полю, в котором находится дата выдачи.

Как можно заметить данные в нем хранятся в виде: месяцMгод.

Соответственно можно просто убрать букву «M» и привести поле к числовому типу. Но если хорошо подумать, то это поле можно удалить, т.к. на один месяц в году может приходиться несколько подразделений выдававших паспорт, и соответственно это может испортить нашу модель. Исходя из этого удалим его из выборки:

train = train.drop(['passport_issue_month/year'], axis=1)




Теперь мы можем перейти к анализу данных.

Анализ данных




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

Авторы пакета scikit-learn заботливо о нас позаботились и добавили несколько способов для извлечения и кодирования текстовых данных. Из них мне больше всего нравятся два:


  1. FeatureHasher

  2. CountVectorizer

  3. HashingVectorizer




FeatureHasher преобразовывает строку в числовой массив заданной длинной с помощью хэш-функции (32-разрядная версия Murmurhash3)

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

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














Значение
раз два три
три четыре два два
раз раз раз четыре



Для начала CountVectorizer собирает уникальные ключи из всех записей, в нашем примере это будет:

[раз, два, три, четыре]


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


раз два три --> [1,1,1,0]

три четыре два два --> [0,2,1,1]


Соответственно после кодировки, применения данного метода мы получим:















Значение
1,1,1,0
0,2,1,1
3,0,0,1



HashingVectorizer является смесью двух выше описанных методов. В нем можно и регулировать размер закодированной строки (как в FeatureHasher) и настраивать токенизатор (как в CountVectorizer). К тому же его производительность ближе к FeatureHasher.

Итак, вернемся к анализу. Если мы посмотрим по внимательнее на наш набор данных то можно заметить, что есть похожие строки но записанные по разному например: "… республика карелия..." и "… по республике карелия...".

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

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

def f_tokenizer(s):
morph = pymorphy2.MorphAnalyzer()
if type(s) == unicode:
t = s.split(' ')
else:
t = s
f = []
for j in t:
m = morph.parse(j.replace('.',''))
if len(m) <> 0:
wrd = m[0]
if wrd.tag.POS not in ('NUMR','PREP','CONJ','PRCL','INTJ'):
f.append(wrd.normal_form)
return f




Функция делает следующее:


  • Сначала она преобразовывает строку в список

  • Затем для всех слов производит разбор

  • Если слово является числительным, предикативном, предлогом, союзом, частицей или междометием не включаем его в конечный набор

  • Если слово не попало в предыдущий список, берем его нормальную форму и добавляем в финальный набор




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

coder = HashingVectorizer(tokenizer=f_tokenizer, n_features=256)




Как можно заметить при создании метода кроме токенизатора мы задаем еще один параметр n_features. Через данный параметр задается длина закодированной строки (в нашем случае строка кодируется при помощи 256 столбцов). Кроме того, у HashingVectorizer есть еще одно преимущество перед CountVectorizer, но сразу может выполнять нормализацию значений, что хорошо для таких алгоритмов, как SVM.

Теперь применим наш кодировщик к обучающему набору:

TrainNotDuble = train.drop_duplicates()
trn = coder.fit_transform(TrainNotDuble.passport_issuer_name.tolist()).toarray()




Построение модели




Для начала нам надо задать значения для столбца, в котором будут содержаться метки классов:

target = TrainNotDuble.passport_div_code.values




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

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



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

В пакете scikit-learn для этого есть функция: accuracy_score

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

pca = PCA(n_components = 15)
trn = pca.fit_transform(trn)




Модель будет выглядеть так:

model = RandomForestClassifier(n_estimators = 100, criterion='entropy')

TRNtrain, TRNtest, TARtrain, TARtest = train_test_split(trn, target, test_size=0.4)
model.fit(TRNtrain, TARtrain)
print 'accuracy_score: ', accuracy_score(TARtest, model.predict(TRNtest))




accuracy_score: 0.6523456

Заключение




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

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

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

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


1 комментарий:

  1. very well explained. I would like to thank you for the efforts you had made for writing this awesome article. This article inspired me to read more. keep it up.
    bag of words using python
    Logistic Regression explained
    Correlation vs Covariance
    Simple Linear Regression
    KNN Algorithm

    ОтветитьУдалить