Перед тем как перейти к описанию практической реализации проекта, важно понять основную технологию, лежащую в его основе — искусственные нейронные сети (ИНС).
Проще говоря, нейронная сеть — это математическая модель, вдохновленная работой человеческого мозга. Её основная задача — находить сложные зависимости и закономерности в данных (например, на изображениях, в тексте или числах), чтобы на основе этого делать прогнозы или принимать решения.
Представьте, что вы учите маленького ребенка распознавать кошку.
- Вы не объясняете ему сложных правил (есть усы, хвост, четыре лапы).
- Вы просто показываете много разных картинок с кошками и без них, каждый раз говоря: «Это кошка», «Это не кошка».
- Мозг ребенка сам, методом проб и ошибок, выявляет закономерности — какие сочетания форм, цветов и деталей соответствуют понятию «кошка».
- Чем больше примеров он увидит, тем точнее будет его распознавание, даже если он встретит кошку необычной породы или в странной позе.
- Данные — это примеры. Вместо картинок с кошками мы даем сети тысячи изображений с текстом и соответствующие им текстовые метки.
- Обучение — это процесс настройки. Сеть начинает делать предположения, сравнивает их с правильным ответом и постепенно настраивает внутренние параметры (так называемые «веса»), чтобы уменьшать количество ошибок.
- Результат — это обобщение. После обучения сеть не просто запоминает примеры, а выучивает общие принципы: как выглядят буквы «А», «Б», «В» в разных шрифтах, размерах и условиях освещения.
Базовый строительный блок — нейрон. Его можно представить как маленькую фабрику, которая:
- Получает на вход несколько сигналов (например, данные о яркости пикселей изображения).
- Взвешивает их (определяет, насколько важен каждый сигнал).
- Суммирует взвешенные сигналы.
- Принимает решение, достаточно ли сильная эта сумма, чтобы «активироваться» и передать сигнал дальше.
Сотни тысяч таких нейронов объединяются в слои:
- Входной слой получает исходные данные (пиксели изображения).
- Скрытые слои — это «мозг» сети. Именно в них происходят сложные вычисления по поиску закономерностей. Чем больше слоев, тем более сложные зависимости может обнаружить сеть (такие сети называют «глубокими»).
- Выходной слой выдает конечный результат (например, распознанную букву).
Задача распознавания текста на изображении идеально подходит для нейронных сетей, потому что:
- Это сложно прописать вручную. Невозможно написать простые правила для всех шрифтов, поворотов, освещения и повреждений текста.
- Есть огромное количество данных. Миллионы изображений с текстом можно использовать для обучения.
- Нейронная сеть может научиться игнорировать лишнее: фон, тени, узоры — и концентрироваться именно на контурах символов и слов.
Многослойный перцептрон Multi-Layer Perceptron, MLP — это класс искусственных нейронных сетей прямого распространения feedforward neural networks, состоящий из множества слоев нейронов. В отличие от однослойного перцептрона, MLP способен решать нелинейные задачи благодаря наличию скрытых слоев.
- Входной слой
Input Layer: получает исходные данные - Скрытые слои
Hidden Layers: выполняют основную обработку информации - Выходной слой
Output Layer: формирует конечный результат
Каждый нейрон в слое соединен со всеми нейронами следующего слоя (полносвязная сеть).
Рассмотрим
где:
-
$z_i^l$ - взвешенная сумма для нейрона$i$ в слое$l$ -
$w_{ij}^l$ - вес связи между$j$ -м нейроном слоя$l-1$ и$i$ -м нейроном слоя$l$ -
$a_j^{(l-1)}$ - активация$j$ -го нейрона предыдущего слоя$(l-1)$ -
$b_i^l$ - смещение (bias)$i$ -го нейрона в слое$l$ -
$n$ - количество нейронов в предыдущем слое$(l-1)$
где:
-
$a_i^l$ - активация (выходное значение) нейрона$i$ в слое$l$ -
$\varphi$ - функция активации -
$z_i^l$ - взвешенная сумма входов нейрона$i$ в слое$l$
Функция:
Производная:
Процесс вычисления выхода сети для заданного входа:
Алгоритм:
- Инициализация:
$a¹ = x$ (входной вектор) - Для каждого слоя
$l = 2$ до$L$ :
где:
-
$z^l$ - вектор взвешенных сумм слоя$l$ -
$W^l$ - матрица весов слоя$l$ -
$a^{(l-1)}$ - вектор активаций предыдущего слоя$(l-1)$ -
$b^l$ - вектор смещений слоя$l$ -
$φ$ - функция активации (применяется поэлементно)
- Результат:
$y_pred = a^L$
где:
-
$W^l$ — матрица весов слоя$l$ -
$b^l$ — вектор смещений слоя$l$ -
$L$ — количество слоев
Для оценки ошибки сети используется функция потерь. Для задачи регрессии — среднеквадратичная ошибка (MSE):
Для задачи классификации — перекрестная энтропия:
Цель: вычислить градиенты функции потерь по параметрам сети для их последующего обновления.
Обозначения:
-
$δ_i^l$ — ошибка$i$ -го нейрона в слое$l$ -
$\frac{\partial J}{\partial w_{ij}^l}$ - градиент по весу$w_{ij}^l$ -
$\frac{\partial J}{\partial b_i^l}$ - градиент по смещению$b_i^l$
Алгоритм:
- Вычисление ошибки выходного слоя:
где
- Обратное распространение по слоям (
$l = L-1$ до$2$ ):
- Вычисление градиентов:
Обновление параметров:
где learning rate
Пакетный градиентный спуск (Batch Gradient Descent):
- Использует весь набор данных для одного обновления
- Стабильная сходимость, но медленная для больших dataset ов
Стохастический градиентный спуск (Stochastic Gradient Descent):
- Обновление после каждого примера
- Быстрая сходимость, но высокая дисперсия
Батчевый градиентный спуск (Mini-batch Gradient Descent):
- Компромиссный вариант
batch size = 32-512 - Наиболее популярный на практике
Проблема исчезающих градиентов: В глубоких сетях с сигмоидой/tanh градиенты могут становиться очень маленькими, что останавливает обучение.
Решения:
- Использование
ReLU-активаций - Инициализация весов (
Xavier,He)
Переобучение (Overfitting): Сеть запоминает обучающие данные, но плохо обобщает.
Методы борьбы:
- Встряска весов (
Dropout) - Ранняя остановка (
early stopping) - Увеличение данных (
data augmentation)
Для обучения модели необходимо:
- Алфавит состоящий из кириллицы, представленный в обоих регистрах, и цифры со специальными символами:
АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя0123456789.,;:!?()-«» - Множество шрифтов для кириллицы, помещённые в папку
fonts
Генерация материалов для обучения:
# dataset.py
from PIL import Image, ImageDraw, ImageFont
import numpy
import os
SYMBOLS = [i for i in "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя0123456789.,;:!?()-«»"]
FONTS = os.listdir("fonts")
x, y = [], []
done = 1
for symbol in SYMBOLS:
for font in FONTS:
font = ImageFont.truetype(os.path.join("fonts", font), 25)
img = Image.new("L", (40, 40), color=0)
draw = ImageDraw.Draw(img)
draw.text((10, 10), symbol, font=font, fill=255)
img = img.crop(img.getbbox())
img = img.resize((20, 20))
arr = numpy.array(img)
arr = (arr > 100).astype(numpy.uint8)
x.append(arr.flatten())
y.append(symbol)
print(f"\r{done}/{len(SYMBOLS) * len(FONTS)}", end="")
done += 1
x = numpy.array(x, dtype=numpy.uint8)
y = numpy.array(y)
numpy.savez("dataset.npz", x=x, y=y)
print()Код создаёт архив с паттернами (рисунки с символами - буквами, цифрами и спец. символами) и сохроняет в файл dataset.npz.
Просмотр содержимого dataset.npz:
# viewer.py
import numpy
import os
data = numpy.load("dataset.npz")
x, y = data["x"], data["y"]
i = 0
while True:
os.system("clear")
for row in x[i].reshape(20, 20):
print("".join("██" if px == 1 else " " for px in row))
print(y[i])
# input()
i = (i + 1) % len(x)Код показывает каждое сгенерированное изображение для обучения.
Обучение нейросети:
# train.py
import tensorflow
import numpy
data = numpy.load("dataset.npz")
x, y = data["x"], data["y"]
symbols = numpy.unique(y)
symbol_to_idx = {s: i for i, s in enumerate(symbols)}
y = numpy.array([symbol_to_idx[s] for s in y])
num_classes = len(symbols)
y = tensorflow.keras.utils.to_categorical(y, num_classes)
indexes = numpy.arange(len(x))
numpy.random.shuffle(indexes)
x = x[indexes]
y = y[indexes]
split = int(0.9 * len(x))
x_train, x_val = x[:split], x[split:]
y_train, y_val = y[:split], y[split:]
model = tensorflow.keras.Sequential([
tensorflow.keras.layers.Input(shape=(400,)),
tensorflow.keras.layers.Dense(512, activation="relu"),
tensorflow.keras.layers.BatchNormalization(),
tensorflow.keras.layers.Dropout(0.5),
tensorflow.keras.layers.Dense(256, activation="relu"),
tensorflow.keras.layers.BatchNormalization(),
tensorflow.keras.layers.Dropout(0.5),
tensorflow.keras.layers.Dense(128, activation="relu"),
tensorflow.keras.layers.BatchNormalization(),
tensorflow.keras.layers.Dropout(0.4),
tensorflow.keras.layers.Dense(64, activation="relu"),
tensorflow.keras.layers.Dropout(0.3),
tensorflow.keras.layers.Dense(num_classes, activation="softmax")
])
model.compile(
optimizer=tensorflow.keras.optimizers.Adam(learning_rate=0.001),
loss="categorical_crossentropy",
metrics=["accuracy"]
)
reduce_lr = tensorflow.keras.callbacks.ReduceLROnPlateau(
monitor='val_accuracy',
factor=0.1,
patience=3,
min_lr=0.000001
)
early_stopping = tensorflow.keras.callbacks.EarlyStopping(
monitor='val_accuracy',
patience=20,
restore_best_weights=True
)
model.fit(x_train, y_train,
validation_data=(x_val, y_val),
epochs=300,
batch_size=64,
shuffle=True,
callbacks=[early_stopping, reduce_lr],
verbose=1
)
model.save("model.keras")
numpy.savez("labels.npz", symbols=symbols)Код обучает модель на основе подготовленных изображений с символами и сохроняет модель в model.npz. tensorflow предоставляет возможность быстро и удобно обучать нейросети на любой архитектуре.
Результат обучения:
# accuracy.py
import tensorflow
import numpy
data = numpy.load("dataset.npz")
x, y = data["x"], data["y"]
model = tensorflow.keras.models.load_model("model.keras")
labels = numpy.load("labels.npz")["symbols"]
predict = model.predict(x, batch_size=64, verbose=0)
predict = labels[numpy.argmax(predict, axis=1)]
for label in labels:
mask = y == label
total = mask.sum()
correct = (predict[mask] == y[mask]).sum()
acc = correct / total * 100
predictions = predict[mask & (predict != y)]
info, counts = numpy.unique(predictions, return_counts=True)
info_list = []
for info_label, count in zip(info, counts):
info_list.append(f"{info_label}: {count / total * 100:.1f}%")
info_list.sort(key=lambda x: float(x.split(": ")[1][:-1]), reverse=True)
print(f"{label}: {correct}/{total} - {acc:.2f}% ({', '.join(info_list)})")Код наглядно показывает как хорошо обучилась модель на обучающем архиве, где видно что модель хорошо различает буквы, цифры и спец. символы друг от друга, но путает регистр. Общий результат обучения получается приемлимым для печатных букв (т.е. для документов), что означает о готовности модели распозновать символы.
- Модель отлично обобщает признаки символов и их распознаёт!
- Мы умпешно создали архив с символами разным шрифтом на изображениях
- Разобрались в работе нейросетей
- Успешно обучили модель используя MLP архитектуру
- Протестировали модель и посмотрели результаты
Но есть одна серьёзная проблемма. Классификация символов, состоящие из нескольких эллементов: ё, й, !, ; и т.д. Проблема связана с тем что для работы модели нужно давать изображения определённого формата и получать результат распознования только одного символа на изображении. А это значит что зоны символов нужно алгоритмически найти на изображении и обрезать их до символа, но сделать это когда символ состоит из нескольких эллементов крайне трудно и энергозатратно, что замедляет работу программы.
Поэтому для распознавания текста на изображении удобнее давать нейросети распознать целое слово в области изображения. Об этом способе рассказывается в след. главе.
Модель, основанная на комбинации сверточных слоёв CNN, рекуррентных слоёв LSTM и функции потерь CTC, является одной из наиболее эффективных архитектур для распознавания текста на изображениях. В отличие от простых моделей, таких как MLP, данная архитектура способна учитывать пространственные особенности изображения, последовательную природу текста и неопределённость в выравнивании символов. Это делает её стандартом в современных OCR‑системах.
- CNN извлекла визуальные признаки из изображения, превратив его в последовательность векторов.
- LSTM обработала эту последовательность, учитывая контекст слева и справа.
- CTC сопоставила выход модели с целевой строкой без необходимости точной разметки каждого символа.
где:
CNN
LSTM
CNN — это фундамент модели. Она преобразует изображение в последовательность признаков, сохраняя важную информацию о форме, контрасте, изгибах и текстуре символов.
где:
- Выделение контуров
- Подавление шума
- Извлечение локальных паттернов
- Формирование устойчивого представления символов
- Шум
- Размытие
- Наклон
- Низкое разрешение
- Артефакты сжатия
После CNN мы получаем последовательность признаков, но каждый вектор сам по себе не содержит информации о соседних символах. Чтобы учитывать контекст, используется двунаправленный LSTM:
- Понимать, что символы образуют слова
- Различать похожие буквы по контексту
- Корректно интерпретировать последовательности разной длины
- «П» и «Л» могут быть похожи по форме, но контекст делает их различимыми
- «0» и «O» различаются по окружению
- «1» и «I» становятся различимыми в зависимости от соседних символов
CTC (Connectionist Temporal Classification) — ключевой элемент, позволяющий обучать модель без точной разметки символов по координатам.
CTC решает задачу сопоставления последовательности признаков
- Повторяющиеся символы
- Пропуски
- Пустые промежутки
- Различную длину входа и выхода
--hhhee_ll_llooo---
hello
где «–» — пустой символ
- гибким
- устойчивым
- независимым от точной разметки
- Учитывает пространственные и последовательные зависимости
CNN анализирует изображение, LSTM — последовательность. Это даёт точность, недостижимую для MLP.
- Не требует покадровой разметки
CTC автоматически выравнивает символы.
- Работает с текстом любой длины
Ширина изображения не ограничивает модель.
- Устойчива к шумам
CNN выделяет устойчивые признаки даже при плохом качестве.
- Подходит для реальных OCR‑задач
- Google OCR,
- Tesseract 4+
- PaddleOCR
- EasyOCR
- Гарантирует высокую точность
При достаточном количестве данных модель достигает стабильных результатов и корректно распознаёт текст даже в сложных условиях.
-
Сложнее и тяжелее, чем MLP
Требует больше вычислительных ресурсов.
-
Дольше обучается
Особенно на больших датасетах.
-
Требует больше данных
Для устойчивой генерализации нужно много примеров.
-
Реализация сложнее
CNNLSTMCTC-декодер- Препроцессинг изображений
Несмотря на сложность, данная архитектура идеально подходит для нашей задачи.
- Высокую точность
- Устойчивость к шумам
- Корректную работу с текстом произвольной длины
- Отсутствие необходимости в разметке символов по координатам
- Стабильность на реальных данных
Обучение такой модели гарантирует корректное распознавание текста в большинстве практических случаев. Для нашего проекта это критически важно.
- Уже оптимизирована
- Протестирована
- Показывает высокую точность
- Полностью закрывает наши требования
Реализацию, подключение и инференс я опишу в отдельном разделе.
В предыдущих главах мы разобрали архитектуру CNN + LSTM + CTC и поняли, почему она идеально подходит для задач распознавания текста. Теперь перейдём к практической части: выберем библиотеку, реализующую эту архитектуру, протестируем её, создадим собственный алгоритм постобработки текста и интегрируем всё это в Telegram‑бота.
Ниже приведена таблица сравнения наиболее распространённых библиотек OCR, которые используют современные нейросетевые архитектуры (включая CNN + LSTM + CTC):
| Библиотека | Архитектура | Языки | Простота | Плюсы | Минусы | Вывод |
|---|---|---|---|---|---|---|
Tesseract 4+ |
LSTM‑OCR |
много | средняя | классика, много гайдов, CLI |
чувствителен к качеству, сложный тюнинг | подходит для простых задач |
EasyOCR |
CNN + LSTM + CTC |
много (RU) |
высокая | простая установка, готовые модели, Python |
требует PyTorch |
лучший выбор для нашего проекта |
PaddleOCR |
CNN + RNN + CTC/Attention |
много | средняя | высокая точность, много моделей | сложнее интеграция, тяжеловат | хорош для крупных проектов |
Google Vision |
проприетарные модели | много | высокая | отличное качество, облачный сервис | платно, зависимость от внешнего API |
не подходит для локального решения |
- Простота интеграции (
Python‑скрипты,Telegram‑бот) - Поддержка русского языка
- Готовая предобученная модель
- Минимум возни с установкой
По этим критериям EasyOCR - наиболее удобный выбор. Она уже реализует архитектуру, близкую к CNN + LSTM + CTC, имеет предобученные модели и легко подключается в Python‑код.
pip install easyocr
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
(для CPU‑версии; для GPU будет другая команда, но в рамках нашего проекта достаточно CPU).
import easyocr
reader = easyocr.Reader(['ru'])
results = reader.readtext('image.jpg')
for coords, text, conf in results:
print(text, conf)EasyOCR возвращает текст фрагментами, и порядок может быть нарушен.
- Вычисляет центры боксов
- Сортирует по вертикали
- Объединяет строки по горизонтали
- Формирует читаемый текст
def group(results):
if not results:
return []
texts, x, y, w, h = [], [], [], [], []
for coordinate, text, _ in results:
x_arr = [px[0] for px in coordinate]
y_arr = [px[1] for px in coordinate]
texts.append(text.strip())
x.append(sum(x_arr) / 4.0)
y.append(sum(y_arr) / 4.0)
w.append(max(x_arr) - min(x_arr))
h.append(max(y_arr) - min(y_arr))
def sort_key_index(i):
return (y[i], x[i])
def sort_key_x(j):
return x[j]
sort_indexes = sorted(range(len(texts)), key=sort_key_index)
lines, current_line = [], []
for i in sort_indexes:
if not current_line:
current_line = [i]
continue
if abs(y[i] - y[current_line[-1]]) <= np.mean(h) * 0.6:
current_line.append(i)
else:
current_line.sort(key=sort_key_x)
lines.append(current_line)
current_line = [i]
if current_line:
current_line.sort(key=sort_key_x)
lines.append(current_line)
return "\n".join([" ".join(texts[j] for j in line) for line in lines])Этот алгоритм делает текст структурированным и удобным для чтения.
Рис. 3: Изображение с кириллицей
Зима
Наступила уже настоящая зима.
Земля была покрыта белоснежным ковром. Не осталось ни одного тёмного пятнышка. Даже голые берёзы, ольхи и рябины убрались инеем, точно серебристым пухом.
Они стояли, засыпанные снегом, как будто надели дорогую тёплую шубу...
Д.Н.Мамин-Сибиряк
Рис. 4: Изображение с латиницей
Hello, boys and girls!
I'm Tom. I live in London. It was a lot of fun last week. It was the Day of the London. I and my mum were in the park. There were a lot of balloons, food and drink. It was very interesting there.
We were happy.
Write back and tell me about your Day of the City.
Send a picture!
Love,
- Принимает изображение
- Распознаёт текст через
EasyOCR - Группирует строки через
group() - Отправляет результат пользователю
import easyocr
import os
import numpy as np
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
reader = easyocr.Reader(['ru'])
def group(results):
if not results:
return []
texts, x, y, w, h = [], [], [], [], []
for coordinate, text, _ in results:
x_arr = [px[0] for px in coordinate]
y_arr = [px[1] for px in coordinate]
texts.append(text.strip())
x.append(sum(x_arr) / 4.0)
y.append(sum(y_arr) / 4.0)
w.append(max(x_arr) - min(x_arr))
h.append(max(y_arr) - min(y_arr))
def sort_key_index(i):
return (y[i], x[i])
def sort_key_x(j):
return x[j]
sort_indexes = sorted(range(len(texts)), key=sort_key_index)
lines, current_line = [], []
for i in sort_indexes:
if not current_line:
current_line = [i]
continue
if abs(y[i] - y[current_line[-1]]) <= np.mean(h) * 0.6:
current_line.append(i)
else:
current_line.sort(key=sort_key_x)
lines.append(current_line)
current_line = [i]
if current_line:
current_line.sort(key=sort_key_x)
lines.append(current_line)
return "\n".join([" ".join(texts[j] for j in line) for line in lines])
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("Привет! Отправь мне изображение, и я распознаю текст на нём.")
async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
photo = update.message.photo[-1]
file = await photo.get_file()
file_path = f"dataset/temp_{len(os.listdir('dataset'))}.jpg"
await file.download_to_drive(file_path)
results = reader.readtext(file_path)
text = group(results)
os.remove(file_path)
if text == []:
await update.message.reply_text("Не удалось распознать текст 😔")
else:
await update.message.reply_text(text)
def main():
telegram = Application.builder().token("TOKEN").build()
telegram.add_handler(CommandHandler("start", start))
telegram.add_handler(MessageHandler(filters.PHOTO, handle_photo))
telegram.run_polling()
if __name__ == "__main__":
os.system("mkdir dataset")
main()- Пользователь отправляет изображение боту
EasyOCRраспознаёт текст- Алгоритм
group()сортирует и объединяет строки - Бот отправляет готовый читаемый текст пользователю
- Локальную систему
OCR - Основанную на
CNN + LSTM + CTC - С постобработкой текста
- Интегрированную в
Telegram‑бота
Это завершённый рабочий проект, который можно развивать дальше.
Проект уже выполняет полный цикл: OCR → постобработка → Telegram‑бот. Однако его можно расширять дальше:
- Улучшение качества
OCR
- Добавление новых языков.
- Предобработка изображений (резкость, выравнивание, шумоподавление).
- Использование
GPUдля ускорения.
- Расширение постобработки
- Исправление типичных
OCR‑ошибок. - Восстановление структуры текста (абзацы, списки).
- Нормализация пунктуации и пробелов.
- Поддержка
PDF
- Извлечение страниц.
OCRкаждой страницы.- Сбор текста в единый документ.
- Улучшение
Telegram‑бота
- Кнопки и меню.
- Выбор языка распознавания.
- Поддержка
PDF, документов, сканов.
- Собственная модель
- Сбор датасета.
- Обучение своей
CNN+LSTM+CTCмодели. - Оптимизация под конкретные документы.
- Внедрение языковой модели
- Исправление граматики.
- Исправление синтаксиса.
- Более точный распознанный текст.
Списибо! - macosnik



