Named Entity Recognition for Russian

Note

This section, “Working in Languages Beyond English,” is co-authored with Quinn Dombrowski, the Academic Technology Specialist at Stanford University and a leading voice in multilingual digital humanities. I’m grateful to Quinn for helping expand this textbook to serve languages beyond English.

In this lesson, we’re going to learn about a text analysis method called Named Entity Recognition (NER) as applied to Russian. This method will help us computationally identify people, places, and things (of various kinds) in a text or collection of texts.


Dataset

The example text for Russian is Яблони цветут from Новые люди by Зинаида Николаевна Гиппиус from Библиотека русской и советской классики. (Thanks to Katherine Bowers for the text referral.)

Here’s a preview of spaC’s NER tagging Яблони цветут.

If you compare the results to the English example, you’ll notice that the Russian NER is much less good at recognizing entities, and is especially bad ata distinguishing different kinds of entities, like ORG vs LOC. You need a lot of examples to train a model to distinguish different entity types; currently, English is the only model that does a decent job of it.

You can read more about the data sources used to train Russian on the spaCy model page.

ает, особенно теперь, ночью… Старуха, моя кухарка, спит. И главное, ничего не сделается от того, что я закину веревку на крюк. Ее можно снять и снести обратно в переднюю. Даже если я петлю сделаю – и то ничего ровно не случится, ведь не повешусь же, ведь не должен же я, оттого что сделаю петлю, непременно повеситься? Это так ужасно, так некрасиво… Как я далеко от Марты PER !.. Но разве я в самом деле?.. Нет, нет, я только попробую, никто не узнает, а я попробую…

NER with spaCy

If you’ve already used the pre-processing notebook for this language, you can skip the steps for installing spaCy and downloading the language model.

Install spaCy

Russian models are only available starting in spaCy 3.0.

If you run into errors because spaCy 2.x is installed, you can run !pip uninstall spacy -y first, then run the cell below.

!pip install -U spacy

Import Libraries

We’re going to import spacy and displacy, a special spaCy module for visualization.

import spacy
from spacy import displacy
from collections import Counter
import pandas as pd
pd.options.display.max_rows = 600
pd.options.display.max_colwidth = 400

We’re also going to import the Counter module for counting people, places, and things, and the pandas library for organizing and displaying data (we’re also changing the pandas default max row and column width display setting).

Download Language Model

Next we need to download the Russian-language model (ru_core_news_md), which will be processing and making predictions about our texts. You can read more about the data sources used to train Russian on the spaCy model page.

!python -m spacy download ru_core_news_md

Load Language Model

Once the model is downloaded, we need to load it. There are two ways to load a spaCy language model.

1. We can import the model as a module and then load it from the module.

import ru_core_news_md
nlp = ru_core_news_md.load()

2. We can load the model by name.

#nlp = spacy.load('ru_core_news_md')

If you just downloaded the model for the first time, it’s advisable to use Option 1. Then you can use the model immediately. Otherwise, you’ll likely need to restart your Jupyter kernel (which you can do by clicking Kernel -> Restart Kernel… in the Jupyter Lab menu).

Process Document

We first need to process our document with the loaded NLP model. Most of the heavy NLP lifting is done in this line of code.

After processing, the document object will contain tons of juicy language data — named entities, sentence boundaries, parts of speech — and the rest of our work will be devoted to accessing this information.

In the cell below, we open and the example document. Then we runnlp() on the text and create our document.

filepath = '../texts/ru.txt'
text = open(filepath, encoding='utf-8').read()
document = nlp(text)

Get Named Entities

All the named entities in our document can be found in the document.ents property. If we check out document.ents, we can see all the entities from the example document.

document.ents
(Яблони,
 Бывало,
 Петербурге,
 Петербурге,
 Москву,
 Знаешь,
 Володя,
 Вот Сыромятников,
 Маремьянов,
 Мама,
 Москву,
 Москве,
 Малой Никитской,
 Близко,
 Мама,
 Володя,
 Москве,
 Листьев,
 Здравствуйте,
 Коренева,
 Коренева,
 Кореневой,
 Марта,
 Марта,
 Марта,
 Володя,
 Марта,
 Марты,
 Марта,
 Марта,
 Голос,
 Марта,
 Солнце,
 Мартой,
 Марты,
 Лениво,
 Володя,
 Марфу Кореневу,
 Мысли,
 Марту,
 Марте,
 Марта,
 Володя,
 Марту,
 Простите,
 Марта,
 Марта,
 Марта,
 Марта,
 Небо,
 Марта,
 Марта,
 Марты,
 Марта,
 Пора,
 Яблони,
 Марта,
 Москвы,
 Марте,
 Петербурге,
 Марты)

Each of the named entities in document.ents contains more information about itself, which we can access by iterating through the document.ents with a simple for loop.

For each named_entity in document.ents, we will extract the named_entity and its corresponding named_entity.label_.

for named_entity in document.ents:
    print(named_entity, named_entity.label_)
Яблони PER
Бывало PER
Петербурге LOC
Петербурге LOC
Москву LOC
Знаешь LOC
Володя PER
Вот Сыромятников PER
Маремьянов PER
Мама PER
Москву LOC
Москве LOC
Малой Никитской LOC
Близко PER
Мама PER
Володя PER
Москве LOC
Листьев PER
Здравствуйте PER
Коренева PER
Коренева PER
Кореневой PER
Марта LOC
Марта LOC
Марта PER
Володя PER
Марта PER
Марты PER
Марта PER
Марта PER
Голос PER
Марта PER
Солнце LOC
Мартой PER
Марты PER
Лениво PER
Володя PER
Марфу Кореневу PER
Мысли PER
Марту PER
Марте PER
Марта LOC
Володя PER
Марту PER
Простите LOC
Марта LOC
Марта PER
Марта PER
Марта PER
Небо PER
Марта PER
Марта PER
Марты PER
Марта PER
Пора PER
Яблони PER
Марта LOC
Москвы LOC
Марте PER
Петербурге LOC
Марты PER

To extract just the named entities that have been identified as PER (person), we can add a simple if statement into the mix:

for named_entity in document.ents:
    if named_entity.label_ == "PER":
        print(named_entity)

NER with Long Texts or Many Texts

import math
number_of_chunks = 80

chunk_size = math.ceil(len(text) / number_of_chunks)

text_chunks = []

for number in range(0, len(text), chunk_size):
    text_chunk = text[number:number+chunk_size]
    text_chunks.append(text_chunk)
chunked_documents = list(nlp.pipe(text_chunks))

Get People

To extract and count the people, we will use an if statement that will pull out words only if their “ent” label matches “PER.”

people = []

for document in chunked_documents:
    for named_entity in document.ents:
        if named_entity.label_ == "PER":
            people.append(named_entity.text)

people_tally = Counter(people)

df = pd.DataFrame(people_tally.most_common(), columns=['character', 'count'])
df
character count
0 Марта 11
1 Володя 5
2 Марты 4
3 Яблони 2
4 Мама 2
5 Коренева 2
6 Марту 2
7 Марте 2
8 Бывало 1
9 Вот Сыромятников 1
10 Маремьянов 1
11 Близко 1
12 Листьев 1
13 Здравствуйте 1
14 Кореневой 1
15 Голос 1
16 Мартой 1
17 Лениво 1
18 Марфу Кореневу 1
19 Мысли 1
20 Полоцк 1
21 Небо 1
22 Пора 1

Get Places

To extract and count places, we can follow the same model as above, except we will change our if statement to check for “ent” labels that match “LOC.”

places = []
for document in chunked_documents:
    for named_entity in document.ents:
        if named_entity.label_ == "LOC":
            places.append(named_entity.text)

places_tally = Counter(places)

df = pd.DataFrame(places_tally.most_common(), columns=['place', 'count'])
df
place count
0 Марта 5
1 Петербурге 3
2 Москву 2
3 Москве 2
4 Знаешь 1
5 Малой Никитской 1
6 Солнце 1
7 Простите 1
8 Москвы 1

Get NER in Context

from IPython.display import Markdown, display
import re

def get_ner_in_context(keyword, document, desired_ner_labels= False):
    
    if desired_ner_labels != False:
        desired_ner_labels = desired_ner_labels
    else:
        desired_ner_labels = ['PER', 'ORG', 'LOC']  
        
    #Iterate through all the sentences in the document and pull out the text of each sentence
    for sentence in document.sents:
        #process each sentence
        sentence_doc = nlp(sentence.text)
        for named_entity in sentence_doc.ents:
            #Check to see if the keyword is in the sentence (and ignore capitalization by making both lowercase)
            if keyword.lower() in named_entity.text.lower()  and named_entity.label_ in desired_ner_labels:
                #Use the regex library to replace linebreaks and to make the keyword bolded, again ignoring capitalization
                #sentence_text = sentence.text
            
                sentence_text = re.sub('\n', ' ', sentence.text)
                sentence_text = re.sub(f"{named_entity.text}", f"**{named_entity.text}**", sentence_text, flags=re.IGNORECASE)

                display(Markdown('---'))
                display(Markdown(f"**{named_entity.label_}**"))
                display(Markdown(sentence_text))
for document in chunked_documents:
    get_ner_in_context('Марта', document)

LOC

– Да, так вы удивились, что я Марта.


PER

Я – Марфа, но Марта


PER

Я не мог убедить себя, что Марта – барышня, а я ей говорю комплименты.


PER

Не успел я сесть на свою скамейку и опомниться, как сейчас же, сию минуту я услышал знакомый шорох, Марта подошла к изгороди и сказала: – Здравствуйте.


PER

– А я знаю, где сегодня спрячется солнце, – сказала Марта.


PER

– Только… – продолжала Марта, – вы не сердитесь, но часто вы играете такое составное, из многих разных нот, а всего-то нет.


PER

– Вы никогда не смеетесь, Марта? – спросил я.


LOC

не, о яблонях, о Марте, и что Марта для меня – оживший сад, то же, что небо и ветер…


LOC

– Простите, Марта, – сказал я.


PER

– Мы будем ждать, – сказала Марта.


PER

– Отчего вы такая, Марта? – спросил я.


PER

– И я люблю вас, Марта, – сказал я.


PER

– Надо спокойнее, спокойнее, – проговорила Марта, положив бледную руку на мою.


PER

– Первый цветок распустился, – сказала Марта.


PER

Когда прошло время, и все кругом нас стало яснее и холоднее, небо позеленело, и утренние сумерки спустились, – я взглянул близко в лицо Марты, Марта сидела все в том же положении, прижавшись ко мне.


LOC

– Прощай, Марта, – сказал я.