Одна из последних технологических тенденций - персональный помощник. Самыми популярными из них являются Siri от Apple, Google Assistant, Cortana от Microsoft и Alexa от Amazon. Вы когда-нибудь задумывались, как можно попросить Сири сообщить вам последние новости или установить вам напоминание, и она точно знает, что делать? Эти технологии используют обработку естественного языка, которая является междисциплинарной областью, которая частично пересекается с информатикой и лингвистикой. Цель состоит в том, чтобы компьютер интерпретировал то, как люди говорят - структуру предложения, синтаксис, грамматику, семантику, - а затем выполнял определенные решения.

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

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

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

Из-за проблем с конфиденциальностью и анонимизацией производственных данных мы решили генерировать собственные «поддельные» данные. Вот пример кода, который показывает, как мы сгенерировали фальшивые данные:

for i in range(2000):
    r = random.random()
    r2 = random.random()
    f = Faker()
    An = random.randint(5,12)
    Bn = random.randint(5,12)
    if r2 > 0.5:
        accountA = ''.join(random.choice(string.ascii_uppercase + string.digits + string.ascii_lowercase) for _ in range(An))
        accountB = ''.join(random.choice(string.ascii_lowercase + string.digits +  string.ascii_lowercase) for _ in range(Bn))
    if r2 < 0.5:
        accountA = ''.join(["%s" % random.randint(0, 9) for num in range(0, 9)])
        accountB = ''.join(["%s" % random.randint(0, 9) for num in range(0, 9)])
    amount = str(random.randint(2000, 10000000))
    if r < 0.05: 
        name = f.name()
        s = "Goodmorning " + name + ", I have a client in need of a money transfer. Could you transfer " + amount + " from account " + accountA + " to account " + accountB +". Please let me know as soon as this is complete. Thanks!"
    elif r >= 0.05 and r < 0.08: 
        name = f.name()
        s = "Hi, I hope this email finds you well. We have an urgent request. Could $" + amount + " be transferred into account " + accountB + " from " + accountA + "? Let me know. Have a great rest of your evening. Best regards," + name

Мы использовали несколько блоков if, elif и else для создания поддельных писем со случайной вероятностью.

Очистка данных

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

def fix_punct(i): 
    i = re.sub(r"([a-z]+)([.()!])", r'\1 ',i)
    i = i.replace(".", " ")
    i = i.replace("?"," ")
    i = i.replace("!", " ")
    i = i.strip()
    return i
df['email'] = df['email'].apply(lambda i: fix_punct(i))

Теперь, когда у нас есть чистые данные, нам нужно вытащить текст и затем назначить метки. Это дает нам что-то вроде этого:

Sample Cleaned Sentence:
Please transfer $ 7926233 SBD from account ciJf to account wIVU  Thank you

Sample Labels:
('7926233', 'ciJf', 'wIVU', 'SBD')

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

Создание функций

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

for sidx, s in enumerate(sentences):
    sent_text = nltk.sent_tokenize(s) # this gives us a list of sentences
    for idx,sentence in enumerate(sent_text):
        sent_labels = []
        all_info = []
        tokenized_text = nltk.word_tokenize(sentence)
        for j in tokenized_text: 
            if j == labels[sidx][0]: 
                sent_labels.append('amount')
            elif j == labels[sidx][1]: 
                sent_labels.append('accountA')
            elif j == labels[sidx][2]: 
                sent_labels.append('accountB')
            elif j == labels[sidx][3]:
                sent_labels.append('currency')
            else: 
                sent_labels.append('O')
        tagged = nltk.pos_tag(tokenized_text)
for idx2, tag in enumerate(tagged): 
            l = (tag[0],tag[1],sent_labels[idx2])
            all_info.append(l)
        pre_feats.append(all_info)

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

def word2features(sent, i):
    word = sent[i][0]
    postag = sent[i][1]
features = {
        'bias': 1.0,
        'word.lower()': word.lower(),
        'word[-3:]': word[-3:],
        'word[-2:]': word[-2:],
        'word.isupper()': word.isupper(),
        'word.istitle()': word.istitle(),
        'word.isdigit()': word.isdigit(),
        'postag': postag,
        'postag[:2]': postag[:2],
    }
    if i > 0:
        word1 = sent[i-1][0]
        postag1 = sent[i-1][1]
        features.update({
            '-1:word.lower()': word1.lower(),
            '-1:word.istitle()': word1.istitle(),
            '-1:word.isupper()': word1.isupper(),
            '-1:postag': postag1,
            '-1:postag[:2]': postag1[:2],
        })
    else:
        features['BOS'] = True
if i < len(sent)-1:
        word1 = sent[i+1][0]
        postag1 = sent[i+1][1]
        features.update({
            '+1:word.lower()': word1.lower(),
            '+1:word.istitle()': word1.istitle(),
            '+1:word.isupper()': word1.isupper(),
            '+1:postag': postag1,
            '+1:postag[:2]': postag1[:2],
        })
    else:
        features['EOS'] = True
return features
def sent2features(sent):
    return [word2features(sent, i) for i in range(len(sent))]
def sent2labels(sent):
    return [label for token, postag, label in sent]
def sent2tokens(sent):
    return [token for token, postag, label in sent]

Это составляет наш вектор характеристик. Теперь мы должны создать наши переменные. X - наша входная переменная, а y - наша выходная переменная. Подумайте о функциях в математике: это отображения, представленные как y = f (X). Именно это мы и делаем здесь: алгоритм - это отображение, и он работает, изучая входные переменные и прогнозируя целевые переменные.

# Create input feature vector
X = [sent2features(i) for i in pre_feats]
# Format the target vector
y = [sent2labels(i) for i in pre_feats]

Создание нашей модели условного случайного поля

Здесь мы создаем нашу модель:

crf = skCRF(
    algorithm='lbfgs',
    max_iterations=100,
    all_possible_transitions=True
)
params_space = {
    
    'c1': scipy.stats.expon(scale=0.5),
    'c2': scipy.stats.expon(scale=0.05),
}

Обратите внимание, что у нас есть пара параметров: алгоритм, max_iterations, c1, c2 и all_possible_transitions. . Мы можем изменить эти параметры, чтобы оптимизировать и улучшить производительность нашей модели, что известно как гиперпараметр настройка.

Обучение / тестирование и оценка эффективности

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

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)
crf.fit(X_train,y_train)

Теперь смотрим отчет о производительности:

Exact Accuracy:  97.04 %

Flat Classification Report:
               precision    recall  f1-score   support

           O       1.00      1.00      1.00     64327
    accountA       0.99      0.98      0.99      2559
    accountB       0.98      0.99      0.99      2732
      amount       1.00      1.00      1.00      2700
    currency       1.00      1.00      1.00      2615

   micro avg       1.00      1.00      1.00     74933
   macro avg       0.99      0.99      0.99     74933
weighted avg       1.00      1.00      1.00     74933

Наши результаты выглядят потрясающе!

Сохранение и запрос модели

Если мы хотим сохранить и повторно использовать то, что изучил алгоритм, мы можем сохранить это в файл .pkl.

filename = 'best_crf_model.pkl'
_ = joblib.dump(crf,filename)

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

class CRF_NER: 
    def __init__(self,model_filepath): 
        self.model_filepath = model_filepath
    def load_model(self): 
        self.crf = joblib.load(self.model_filepath)
        
    def query(self,sentence): 
        original_sentence = sentence
        sentence = re.sub(r"([a-z]+)([.()!])", r'\1 ',sentence)
        sentence = sentence.replace(".", " ")
        sentence = sentence.replace("?"," ")
        sentence = sentence.replace("!", " ")
        sentence = sentence.replace(",", "")
        sentence = sentence.strip()
        sentence = sentence.replace('[',"")
        sentence = sentence.replace(']',"")
        sentence = re.sub('[()""“”{}<>]', '', sentence)
        sentence = re.sub(r"\$([0-9+])", "$ \\1", sentence)
        
        sent_text = nltk.sent_tokenize(sentence)
        tokenized_text = nltk.word_tokenize(sentence)
        tagged = nltk.pos_tag(tokenized_text)
def word2features(sent, i):
            word = sent[i][0]
            postag = sent[i][1]
features = {
                'bias': 1.0,
                'word.lower()': word.lower(),
                'word[-3:]': word[-3:],
                'word[-2:]': word[-2:],
                'word.isupper()': word.isupper(),
                'word.istitle()': word.istitle(),
                'word.isdigit()': word.isdigit(),
                'postag': postag,
                'postag[:2]': postag[:2],
            }
            if i > 0:
                word1 = sent[i-1][0]
                postag1 = sent[i-1][1]
                features.update({
                    '-1:word.lower()': word1.lower(),
                    '-1:word.istitle()': word1.istitle(),
                    '-1:word.isupper()': word1.isupper(),
                    '-1:postag': postag1,
                    '-1:postag[:2]': postag1[:2],
                })
            else:
                features['BOS'] = True
if i < len(sent)-1:
                word1 = sent[i+1][0]
                postag1 = sent[i+1][1]
                features.update({
                    '+1:word.lower()': word1.lower(),
                    '+1:word.istitle()': word1.istitle(),
                    '+1:word.isupper()': word1.isupper(),
                    '+1:postag': postag1,
                    '+1:postag[:2]': postag1[:2],
                })
            else:
                features['EOS'] = True
return features
def sent2features(sent):
            return [word2features(sent, i) for i in range(len(sent))]
def sent2labels(sent):
            return [label for token, postag, label in sent]
def sent2tokens(sent):
            return [token for token, postag, label in sent]
        
        X = [sent2features(i) for i in [tagged]]
        
        pred = self.crf.predict(X)
        p = pred[0]
        result = {}
        for idx,i in enumerate(p): 
            if i != 'O': 
                result[i] = tagged[idx][0]    
                
        keys = list(result.keys())
        vals = list(result.values())
        vals
        for idx, i in enumerate(keys): 
            if i == 'amount': 
                insert_str_0 = "<span style='color:blue'>"
                insert_str_1 = "</span>" 
                sent_idx = original_sentence.find(vals[idx])
                original_sentence = original_sentence[:sent_idx] + insert_str_0 + original_sentence[sent_idx:sent_idx+len(vals[idx])]+insert_str_1+original_sentence[sent_idx+len(vals[idx]):]
            elif i == 'accountA': 
                insert_str_0 = "<span style='color:red'>"
                insert_str_1 = "</span>" 
                sent_idx = original_sentence.find(vals[idx])
                original_sentence = original_sentence[:sent_idx] + insert_str_0 + original_sentence[sent_idx:sent_idx+len(vals[idx])]+insert_str_1+original_sentence[sent_idx+len(vals[idx]):]
elif i == 'accountB':
                insert_str_0 = "<span style='color:orange'>"
                insert_str_1 = "</span>" 
                sent_idx = original_sentence.find(vals[idx])
                original_sentence = original_sentence[:sent_idx] + insert_str_0 + original_sentence[sent_idx:sent_idx+len(vals[idx])]+insert_str_1+original_sentence[sent_idx+len(vals[idx]):]
            elif i == 'currency': 
                insert_str_0 = "<span style='color:green'>"
                insert_str_1 = "</span>" 
                sent_idx = original_sentence.find(vals[idx])
                original_sentence = original_sentence[:sent_idx] + insert_str_0 + original_sentence[sent_idx:sent_idx+len(vals[idx])]+insert_str_1+original_sentence[sent_idx+len(vals[idx]):]
printmd(original_sentence)
        return result

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

filename = "best_crf_model.pkl"
c = CRF_NER(filename)
c.load_model()

НАКОНЕЦ, мы можем протестировать нашу модель, чтобы увидеть, действительно ли она работает. Мы можем написать любой образец текста, который содержит два банковских счета (один мы списываем, а другой зачисляем через платеж), сумму и валюту.

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

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

Использование модели

Теперь, когда у нас есть обученная модель, готовая к использованию, мы разработали приложение Flask для Gmail и Outlook, которое отправляет HTTP-запрос в один из наших существующих продуктов (Finastra FusionFabric.cloud’s Payment Initiation API) для эффективной обработки платежей. Все, что нам нужно сделать, это использовать POST-запрос к API - это действительно так просто! Если вы хотите узнать больше о программном обеспечении, стоящем за всем этим, см. Здесь, где мы подробно расскажем о том, как мы реализовали нашу модель с помощью приложения Flask.

Смотреть вперед

Хотя этот проект был всего лишь пробной версией концепции, мы довольно успешно продемонстрировали, как мы можем применить искусственный интеллект для ускорения процессов, в которых требуется ручной труд. Так что, продвигаясь вперед с более продвинутыми алгоритмами, моделями и наборами данных, мы можем создавать гораздо более эффективные способы справиться с огромным миром неструктурированных данных. Это только верхушка айсберга; представьте себе другие способы повышения производительности труда за счет сокращения ручного труда с помощью НЛП. Какие еще способы вы можете придумать? Мы будем рады услышать ваши мысли и отзывы. Не забудьте прочитать часть 2 :)

Спасибо за чтение! Представляю вам своего партнера по этому проекту:

Адам Либерман получил степень магистра математики и машинного обучения в Технологическом институте Джорджии. Он возглавляет группу по анализу данных в Finastra и с энтузиазмом относится к тому, чтобы проверять концептуальные проекты и превращать их в реальные производственные решения.

Джош Абельман - инженер-программист в инновационной лаборатории Finastra. Он работает с командой специалистов по анализу данных, чтобы помочь воплотить модели в жизнь. Его основные интересы - комплексная разработка и глубокое обучение.