В предыдущей части вы узнали, что качество модели Data Science в первую очередь зависит от исходных данных: растет, если у вас больше исходных переменных и уровней градации каждой из них, более равномерно распределены значения каждой из переменных; если у вас меньше пропущенных значений и они менее скоррелированы друг с другом. И наконец, если ваша модель распознает события из прошлого, а не предсказывает будущее.

В третьей, заключительной части статьи я дам ответы по рисункам из предыдущей части. И для дата-сайентистов приведу общий код Python, который использовался для получения всех представленных ниже изображений.

Код для изображений
# Импортируем модули
from PIL import Image
import numpy as np
from tqdm import tqdm
import os
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
plt.rcParams['figure.figsize'] = (18,7)

def im_centralized(img):    
    """Обрезает рисунок в квадрат
    по меньшей стороне"""    
    
    width, height = img.size

    # рассчитываем точки для обрезки в квадрат 
    # по наименьшей стороне
    if height > width:
        left = 0
        top = int((height - width) / 2)
        right = width
        bottom = width + int((height - width) / 2)
    else:
        left = int((width - height) / 2)
        top = 0
        right = height + int((width - height) / 2)
        bottom = height

    # возвращаем изображение, обрезанное в квадрат 
    # по меньшей стороне
    return img.crop((left, top, right, bottom))

Рисунки, связанные с количеством переменных

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

Код для генерации рисунков
def show_images(im_path, share, nw_size):   
    """Получает путь к изображению, обрезает его в квадрат
    по наименьшей стороне. Приводит к размеру
    nw_size * nw_size точек.
    share точек делает черными, остальные белыми.
    Выводит двухцветное изображение, и рядом с ним - 
    гистограмму распределения цветов.
    Также выводит серое изображение, красит точки,
    оказавшиеся белыми, в розовый; рядом с изображением рисует 
    гистограмму распределения оттенков серого, а также порог,
    разделяющий точки, изображенные на первом рисунке
    белыми и черными"""
    
    # загружаем изображение
    im = Image.open(im_path)

    # обрезаем в квадрат по меньшей стороне
    im_croped = im_centralized(im)

    # переводим в квадрат nw_size * nw_size точек
    im_resized = im_croped.resize((nw_size, nw_size))
    
    # конвертируем изображение в тона серого цвета 
    im_grey = im_resized.convert("L")
    
    # переводим в массив numpy
    im_grey = np.array(im_grey)

    # выделяем три компоненты цвета
    im_red = im_grey.copy()
    im_green = im_grey.copy()
    im_blue = im_grey.copy()

    # значения переменных превращаем в одномерный массив
    ravel_points = im_grey.copy().ravel()

    # сортируем значения переменных
    ravel_points.sort()

    # находим порог, отличающий белых от черных
    threshold = ravel_points[int(share * len(ravel_points))]

    # формируем двухцветное изображение
    im_2_colors = im_grey.copy()
    im_2_colors[im_2_colors <= threshold] = 0
    im_2_colors[im_2_colors > threshold] = 255

    # формируем изображение в серых тонах,
    # подсвечиваем белые точки (на двухцветном изображении)
    # розовым цветом
    im_red[im_red > threshold] = 255
    im_green[im_green > threshold] = 190
    im_blue[im_blue > threshold] = 190

    # добавляем дополнительную ось
    im_red = im_red[:,:,np.newaxis]
    im_green = im_green[:,:,np.newaxis]
    im_blue = im_blue[:,:,np.newaxis]

    # формируем цветное изображение
    im_marked = np.concatenate([im_red, im_green, im_blue], 
                               axis=2) / 255

    # получаем параметры гистограммы
    hist_height, _ = np.histogram(im_grey.ravel(), 64)

    # формируем фигуру
    fig = plt.figure(figsize=(18,12))

    # создаем сетку для двух графиков
    gs = GridSpec(1, 3, figure=fig)
    axs0 = fig.add_subplot(gs[0, :-1])
    axs1 = fig.add_subplot(gs[0, -1])
    
    # выводим изображение в серых тонах,
    # подкрашенное розовым
    axs0.imshow(im_marked)
    # название изображения
    axs0.set_title('Изображение', fontsize=16)
    # шрифт меток оси
    axs0.tick_params(labelsize=14)
    # гистограмма для изображения в серых тонах
    axs1.hist(im_grey.ravel(), 64, color='gray');
    axs1.set_title('Распределение значений переменных', 
                   fontsize=16);
    # рисуем порог
    axs1.plot([threshold, threshold], [0, max(hist_height)], 'r', 
              label='Порог, разделяющий черные и белые точки')
    # выводим легенду
    axs1.legend(fontsize=14);  
    axs1.tick_params(labelsize=14)
    
    # аналогично для двухцветного изображения
    fig = plt.figure(figsize=(18,12))

    gs = GridSpec(1, 3, figure=fig)
    axs0 = fig.add_subplot(gs[0, :-1])
    axs1 = fig.add_subplot(gs[0, -1])
    
    axs0.imshow(im_2_colors, cmap='gray')
    axs0.set_title('Изображение', fontsize=16)
    axs0.tick_params(labelsize=14)
    axs1.hist(im_2_colors.ravel(), 64, color='k');
    axs1.set_title('Распределение значений переменных', 
                   fontsize=16);
    axs1.tick_params(labelsize=14)


im_path = 'путь к файлу с изображением'
# задаем размер рисунка
NW_SIZE = 100

# задаем долю значений черного цвета
SHARE = .95

# выводим изображение
show_images(im_path, SHARE, NW_SIZE)

Ответ к рисунку 3 — вертолет. 1 024 переменных, частота модального значения 95%:

Ответ к рисунку 4 — лошадь. 2 500 переменных, частота модального значения 95%:

Ответ к рисунку 5 — самолет. 5 000 переменных, частота модального значения 95%:

Ответ к рисунку 6 — автобус. 10 000 переменных, частота модального значения 95%:

Ответ к рисунку 7 — скоростной поезд. 20 000 переменных, частота модального значения 95%:

Ответ к рисунку 8 — велосипед. 40 000 переменных, частота модального значения 95%:

Ответ к рисунку 9 — слон. 100 000 переменных, частота модального значения 95%:

Ответ к рисунку 10 — рыба. 250 000 переменных, частота модального значения 95%:

Рисунки, связанные с частотой модального значения

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

Ответ к рисунку 11 — лягушка. 10 000 переменных, частота модального значения 95%:

Ответ к рисунку 12 — парусный корабль. 10 000 переменных, частота модального значения 90%:

Ответ к рисунку 13 — автомобиль. 10 000 переменных, частота модального значения 80%:

Ответ к рис. 14 — тигр. 10 000 переменных, частота модального значения 70%.

Ответ к рис. 15 — трактор. 10 000 переменных, частота модального значения 60%:

Ответ к рисунку 16 — птица. 10 000 переменных, частота модального значения 50%:

Ответ к рисунку 17 — змея. 10 000 переменных, частота модального значения 95%:

Рисунок для демонстрации трех градаций переменных

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

Код для генерации изображений
def images_3_colors(im_path, nw_size):
    
    """Получает путь к изображению, обрезает его в квадрат
    по наименьшей стороне. Приводит к размеру
    nw_size * nw_size точек.
    Делает 1/3 точек черными, 1/3 серыми, 1/3 белыми.
    Выводит трехцветное изображение, и рядом с ним
    гистограмму распределения цветов.
    """
    
    # загружаем изображение
    im = Image.open(im_path)

    # обрезаем в квадрат по меньшей стороне
    im_croped = im_centralized(im)

    # переводим в квадрат nw_size * nw_size точек
    im_resized = im_croped.resize((nw_size, nw_size))
    
    # конвертируем изображение в тона серого цвета 
    im_grey = im_resized.convert("L")
    
    # переводим в массив numpy
    im_grey = np.array(im_grey)

    # значения переменных превращаем в одномерный массив
    ravel_points = im_grey.copy().ravel()

    # сортируем значения переменных
    ravel_points.sort()

    # находим 2 порога, делящие градации цвета
    # на 3 части
    threshold1 = ravel_points[int(1/3 * len(ravel_points))]
    threshold2 = ravel_points[int(2/3 * len(ravel_points))]  
   
   
    # формируем трехцветное изображение
    im_3_colors = im_grey.copy()

    im_3_colors[im_3_colors <= threshold1] = 0
    im_3_colors[im_3_colors > threshold2] = 255
    im_3_colors[(im_3_colors > 0)&(
        im_3_colors <255)] = 127

    # получаем параметры гистограммы
    hist_height, _ = np.histogram(im_grey.ravel(), 64)
    
    
    # формируем фигуру
    fig = plt.figure(figsize=(18,12))

    # создаем сетку для двух графиков
    gs = GridSpec(1, 3, figure=fig)
    axs0 = fig.add_subplot(gs[0, :-1])
    axs1 = fig.add_subplot(gs[0, -1])
    
    # выводим изображение в серых тонах
    axs0.imshow(im_grey, cmap='gray')
    # название изображения
    axs0.set_title('Изображение', fontsize=16)
    # шрифт меток оси
    axs0.tick_params(labelsize=14)
    # гистограмма для изображения в серых тонах
    axs1.hist(im_grey.ravel(), 64, color='gray');
    axs1.set_title('Распределение значений переменных', 
                   fontsize=16);
    # рисуем пороги
    axs1.plot([threshold1, threshold1], [0, max(hist_height)], 'b', 
              label='Порог, разделяющий черные и серые точки')
    axs1.plot([threshold2, threshold2], [0, max(hist_height)], 'r', 
              label='Порог, разделяющий серые и белые точки')
    # выводим легенду
    axs1.legend(fontsize=14);  
    axs1.tick_params(labelsize=14)
    
    # формируем фигуру
    fig = plt.figure(figsize=(18,12))

    # создаем сетку для двух графиков
    gs = GridSpec(1, 3, figure=fig)
    axs0 = fig.add_subplot(gs[0, :-1])
    axs1 = fig.add_subplot(gs[0, -1])

    # выводим трехцветное изображение
    axs0.imshow(im_3_colors, cmap='gray')
    # название изображения
    axs0.set_title('Изображение', fontsize=16)
    # шрифт меток оси
    axs0.tick_params(labelsize=14)
    # гистограмма для изображения в серых тонах
    axs1.hist(im_3_colors.ravel(), 64, color='gray');
    axs1.set_title('Распределение значений переменных', 
                   fontsize=16);
    axs1.tick_params(labelsize=14)
im_path = 'путь к файлу с изображением'
# задаем размер изображения
NW_SIZE = 100
# выводим изображение
images_3_colors(im_path, NW_SIZE)

Ответ к рисунку 18 — овца. 10 000 переменных, три градации с частотой по 33% каждая:

Рисунки с некоррелированными пропусками переменных

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

Код для генерации изображений
def show_noised(im_path, share, nw_size):
    """Изображение по адресу im_path обрезает
    в квадрат по меньшей стороне, переводит 
    в градации серого, а затем удаляет из него 
    долю точек, равную share, путем замены их 
    истинного цвета на средний цвет всего рисунка
    Слева выводит зашумленное изображение, 
    справа - гистограмму распределения цветов"""    
    
    im = Image.open(im_path)
    
    # обрезаем в квадрат по наименьшей стороне
    im_croped = im_centralized(im)
    
    # переводим в квадрат nw_size * nw_size точек
    im_resized = im_croped.resize((nw_size, nw_size))

    # конвертируем файл в тона серого
    im_grey = im_resized.convert("L")   
    

    # конвертируем файл в numpy
    im_grey = np.array(im_grey)

    # формируем файл шума того же размера,
    # что и текущий
    noise = np.random.rand(
        im_grey.shape[0], im_grey.shape[1])

    # формируем зашумленный файл
    im_noised = im_grey.copy()

    # для точек, которым соответствуют значения
    # в случайном файле, превосходящие 1 - share, заменяем 
    # истинный цвет на цвет фона
    im_noised[noise > 1 - share] = im_grey.mean()

    # создаем фигуру
    fig = plt.figure(figsize=(18,12))

    # создаем сетку для двух графиков
    gs = GridSpec(1, 3, figure=fig)
    axs0 = fig.add_subplot(gs[0, :-1])
    axs1 = fig.add_subplot(gs[0, -1])

    # выводим изображение в серых тонах,
    # подкрашенное розовым
    axs0.imshow(im_noised, cmap='gray')
    # название изображения
    axs0.set_title('Изображение', fontsize=16)
    # шрифт меток оси
    axs0.tick_params(labelsize=14)
    # гистограмма для изображения в серых тонах
    axs1.hist(im_noised.ravel(), 64, color='gray');
    axs1.set_title('Распределение значений переменных', 
                   fontsize=16);

    axs1.tick_params(labelsize=14)

im_path = 'путь к файлу с изображением'
# задаем размер рисунка
NW_SIZE = 100

# задаем долю шума
SHARE = .5

# выводим изображение
show_noised(im_path, SHARE, NW_SIZE)

Ответ к рисунку 19 — велосипедистка. 10 000 переменных, доля нескоррелированных пропусков 20%:

Ответ к рисунку 20 — обезьяна. 10 000 переменных, доля нескоррелированных пропусков 40%:

Ответ к рисунку 21 — стиральная машина. 10 000 переменных, доля нескоррелированных пропусков 50%:

Ответ к рисунку 22 — лев. 10 000 переменных, доля нескоррелированных пропусков 60%:

Ответ к рис. 23 — танк. 10 000 переменных, доля нескоррелированных пропусков 70%:

Ответ к рисунку 24 — чашка кофе. 10 000 переменных, доля нескоррелированных пропусков 80%:

Ответ к рисунку 25 — чайник. 10 000 переменных, доля нескоррелированных пропусков 90%:

Рисунки со скоррелированными пропусками переменных

Слева показан зашумленный рисунок, справа — матрица шума. Они имеют одинаковый размер. Матрица шума накладывается на рисунок. Там, где в матрице шума белые точки, на рисунке оригинальное изображение заменено серым цветом. Там, где в матрице шума черные точки, сохранено оригинальное изображение.

Код для генерации изображений
def show_correlated_noise(im_path, nw_size, only_left=True):
    """Обрезает рисунок в квадрат по наименьшему
    размеру, конвертирует в градации серого цвета,
    если only_left=True выводит только пикселизированное
    изображение.
    Если only_left=False, то функция
    выводит слева исходный рисунок с сеткой, 
    соответствующей числу пикселей справа,
    а справа - пикселизированное изображение"""   
 
    
    
    # загружаем изображение
    im = Image.open(im_path)

    # обрезаем в квадрат по наименьшей стороне
    im_croped = im_centralized(im)

    # конвертируем изображение в тона серого цвета 
    im_grey = im_croped.convert("L")

    # переводим в квадрат nw_size * nw_size точек
    im_resized = im_grey.resize((nw_size, nw_size))
    
    # конвертируем файл в numpy
    im_grey1 = np.array(im_resized)

    # формируем файл шума того же размера,
    # что и изображение
    noise = np.random.rand(
        im_grey1.shape[0], im_grey1.shape[1])
    
    # среднее значение 
    # цвета всего серому изображению
    gray_mean = im_grey1.mean()  
    
    # формируем файл со скоррелированным шумом
    noise_correlated = 0 * noise    
    
    # вероятность шума изменяется вдоль
    # ширины рисунка по логистическому 
    # закону: слева почти все точки зашумлены,
    # справа шума почти нет
    for i in tqdm(range(im_grey1.shape[1])):
        # noise_column - одномерный вектор
        # для одновременного вычисления степени зашумленности
        # всех точек с одинаковой координатой по ширине

        # копируем нескоррелированный шум 
        # с данной координатой по ширине
        noise_column = noise[:, i].copy()

        # в зависимости от координаты по ширине с помощью
        # логистической кривой устанавливаем точки, в которых 
        # шум равен 1: чем левее, тем таких точек больше 
        noise_column[noise_column < 1 / (
            1 + np.exp((i / im_grey1.shape[1] - .5)/.05)) ] = 1

        # формируем очередной столбец для скоррелированного шума
        noise_correlated[:, i] = noise_column  
        

        # формируем зашумленное изображение
        im_grey_noised = im_grey1.copy()
        im_grey_noised[noise_correlated == 1] = gray_mean
        
        
    
    if only_left == True:
        # создаем фигуру
        fig = plt.figure(figsize=(8, 8))
        plt.imshow(im_grey1, cmap='gray');
        
        plt.title('Исходное изображение'.format(
            nw_size, nw_size), 
                      fontsize=16);

        plt.tick_params(labelsize=14)    
        
    else:

        # создаем фигуру
        fig = plt.figure(figsize=(18, 9))

        # создаем сетку для двух изображений
        gs = GridSpec(1, 2, figure=fig)
        axs0 = fig.add_subplot(gs[0, 0])
        axs1 = fig.add_subplot(gs[0, 1])

        # выводим зашумленное изображение в серых тонах
        axs0.imshow(im_grey_noised, cmap='gray')

        # название изображения
        axs0.set_title('Зашумленное изображение', fontsize=16)
        # шрифт меток оси
        axs0.tick_params(labelsize=14)
        
        # выводим файл скоррелированного шума
        axs1.imshow(noise_correlated == 1, cmap='gray');

        axs1.set_title('Шум: зашумленные точки показаны белым цветом'.format(
            nw_size, nw_size), 
                       fontsize=16);

        axs1.tick_params(labelsize=14)


im_path = 'путь к файлу с изображением'
# задаем размер рисунка
NW_SIZE = 100

# выводим зашумленное изображение
show_correlated_noise(im_path, NW_SIZE, only_left=False)

# выводим исходное изображение
show_correlated_noise(im_path, NW_SIZE, only_left=True)

Ответ к рисунку 26 — две девушки. 10 000 переменных, доля скоррелированных пропусков 50%:

Ответ к рисунку 27 — две девушки. 1 000 000 переменных, доля скоррелированных пропусков 50%:

Вот и всё!

В заключение пожелаем представителям бизнес-подразделений снабжать дата-сайентистов качественными данными, а дата-сайентистам — строить на них модели с точностью 100 и более процентов (ну это я погорячился :)). Для удобства — ссылки на первую и вторую часть этой статьи.

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