Пользователь
0,0
рейтинг
9 декабря 2013 в 16:20

Разработка → Введение в анализ текстовой информации с помощью 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_code passport_issuer_name passport_issue_month/year
id
1 422008 БЕЛОВСКИМ УВД КЕМЕРОВСКОЙ ОБЛАСТИ 11M2001
2 500112 ТП №2 В ГОР. ОРЕХОВО-ЗУЕВО ОУФМС РОССИИ ПО МО ... 03M2009
3 642001 ВОЛЖСКИМ РОВД ГОР.САРАТОВА 04M2002
4 162004 УВД МОСКОВСКОГО РАЙОНА Г.КАЗАНЬ 12M2002
5 80001 ОТДЕЛОМ ОФМС РОССИИ ПО РЕСП КАЛМЫКИЯ В Г ЭЛИСТА 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_code passport_issuer_name passport_issue_month/year
id
19 100010 отделением уфмс россии по республике карелия в... 04M2008
22 100010 отделением уфмс россии по р. карелия в медвежь... 10M2009
5642 100010 отделением уфмс россии по респ карелия в медве... 08M2008
6668 100010 отделением уфмс россии по республике карелия в... 08M2011
8732 100010 отделением уфмс россии по республике карелия в... 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% близка к угадыванию. Чтобы улучшить нужно при первичной обработке обработать грамматические ошибки и различного рода описки. Данное действие также скажется положительно и на словаре при кодировании поля, т. е. его размер уменьшиться и соответственно уменьшиться длина строки после ее кодировки.
Кроме того этап обучения тестовой выборки опущен специально, т. к. в нем нет ничего особенного, кроме его приведения к нужному виду (это можно легко сделать взяв за основу преобразования обучающей выборки)
В статье я попытался показать минимальный список этапов по обработке текстовой информации для подачи ее алгоритмам машинного обучения. Возможно делающим первые шаги в анализе данных данная информация будет полезной.

UPD: Консоль IPython Notebook TKCTask2Answer.ipynb
@kuznetsovin
карма
55,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (8)

  • +2
    Почему бы не иметь список всех органов (структур), которые выдают паспорта и сделать полне не свободным для заполнения, а выборочным из списка?
    • 0
      Вы представляете их количество?
      • 0
        Не аргумент. Разделить на регионы, на города, на районы.
        • 0
          Уменьшит скорость заполнения по-моему.
        • 0
          лучше всего — набор с подсказками а-ля suggest
  • 0
    *поле, конечно.
  • +2
    По хорошему ПО, через которое вводится данная информация, должно бы проектироваться именно с этим условием. Но задача заключалась именно в анализе выгрузки в таком виде, как описан в статье.
  • 0

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