Введение в анализ текстовой информации с помощью 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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 9

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

                Only users with full accounts can post comments. Log in, please.