🔗 Репозиторий на GitHub

Проект: Прогнозирование температуры звезды¶

Описание проекта:

Обычно для расчёта температуры звезды учёные пользуются следующими методами:

  • Закон смещения Вина.
  • Закон Стефана-Больцмана.
  • Спектральный анализ.

Каждый из них имеет плюсы и минусы. Обсерватория хочет внедрить технологии машинного обучения для предсказания температуры звёзд, надеясь, что этот метод будет наиболее точным и удобным. В базе обсерватории есть характеристики уже изученных 240 звёзд.

Цель проекта: Найти решение задачи регрессии по определению температуры на поверхности обнаруженных звёзд с помощью нейросети и точностью прогноза RMSE <= 4500.

Описание данных:

6_class.csv — описание уже изученных звезд.

Описание признаков:

  • Luminosity(L/Lo) — светимость звезды относительно Солнца;
  • Radius(R/Ro) — радиус звезды относительно радиуса Солнца;
  • Absolute magnitude(Mv) — абсолютная звёздная величина, физическая величина, характеризующая блеск звезды;
  • Star color — (white, red, blue, yellow, yellow-orange и др.) — цвет звезды, который определяют на основе спектрального анализа;
  • Star type — тип звезды (описание типов в таблице ниже);
Тип звезды Номер, соответствующий типу
Коричневый карлик 0
Красный карлик 1
Белый карлик 2
Звёзды главной последовательности 3
Сверхгигант 4
Гипергигант 5
  • Temperature (K)— (Целевой признак) — абсолютная температура на поверхности звезды в Кельвинах.

Ход исследования:

  • Подготовка данных: загрузка и изучение общей информации из представленных датасетов.

  • Предобработка данных: стандартизация данных и обработка явных и неявных дубликатов.

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

  • Корреляционный анализ: изучение взимосвязей между входными признаками и целевыми, а также и между ними.

  • Подготовка данных к обучению модели: деление данных на тестовую и тренировочную выборки, feature engineering, отбор признаков для обучения с помощью mutual_info_regression, построение пайплайна обработки данных.

  • Построение базовой нейронной сети: получение бызовой метрики по тестовой выборки с помощью закона Стефана-Больцмана, построение базовой нейронной сети, где определяется архитектура модели и получение метрики качества ее прогноза на тестовой выборке и анализ результатов.

  • Улучшение нейронной сети: подбор гиперпараметров для ранее полученной нейросети, добавление регуляризации и обучения с помощью батчей, получение метрики качества ее прогноза на тестовой выборке и анализ результатов, тестируется добавление аугментированных данных для улучшения точности прогноза нейросети.

  • Общий вывод: резюмирование полученных результатов, формулировка ключевых выводов.

Подготовка рабочей среды и вспомогательные функции¶

Импорт библиотек и базовые настройки блокнота:¶

In [1]:
# Стандартные библиотеки
import math
import random

# Сторонние библиотеки
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import phik
import scipy.stats as stats
import seaborn as sns
from IPython.display import display
from scipy.stats import anderson
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sklearn.compose import ColumnTransformer
from sklearn.decomposition import FastICA
from sklearn.feature_selection import mutual_info_regression
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import (OneHotEncoder, 
                                   StandardScaler,
                                   FunctionTransformer)
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset

# Базовые настройки блокнота
sns.set()
sns.set_context('paper')
pd.set_option('display.max_column', None)
pd.set_option('display.max_colwidth', None)

# Фиксируем случайность для воспроизводимости результатов
RANDOM_STATE = 6011994
random.seed(RANDOM_STATE)
np.random.seed(RANDOM_STATE)
torch.manual_seed(RANDOM_STATE)
torch.use_deterministic_algorithms(True)

Вспомогательные функции:¶

In [2]:
# Функция для получения общей информации о датафрейме
def gen_info(df):
    '''
    Данная функция выводит общую информацию 
    о датафрейме, статистическое описание признаков
    и 5 рандомных строк.
    На ввод функция принимает переменную датафрейма.
    '''
    # Статистики по количественным признакам
    desc = df.describe().T

    # Вывод результатов
    print(df.info())
    display(desc)
    display(df.sample(5, random_state=RANDOM_STATE))
In [3]:
# Функция для поиска неявных дубликатов
def hidden_dup_search(df):
    '''
    Данная функция приводит значения категориальных 
    столбцов к единому стилю и выводит их уникальные 
    значения.
    На ввод функция принимает переменную датафрейма.
    '''
    
    # Список категориальных признаков
    df_cat_col = df.select_dtypes(include = 'object').columns.tolist()

    # Приводим все значения к единому стилю
    # и проверяем уникальные значения для 
    # точечной проработки при необходимости
    for feature in df_cat_col:
        df[feature] = df[feature].str.lower().str.replace(' ', '_').str.replace('-', '_')
        print(f'Уникальные значения признака: {str(feature)}')
        print(df[feature].unique())
        print()
In [4]:
# Функция для комплексного анализа количественного признака
def analyzis_quantity(df, x_label, y_label='Частота', target=None, 
                      hue=None, size=None, sizes=None, system=False, 
                      discrete=False, log_scale=False, title_hist=None, 
                      title_scatter=None):
    '''
    Данная функция выводит "коробочный" график и гистограмму 
    по указанному столбцу датафрейма и его статистические метрики.
    Аргументы функции:
    df - данные (pd.Series)
    x_label - подпись для оси Х
    y_label - подпись для оси Y (по умолчанию "Частота")
    target - целевая переменная для scatter-графика (если system=True)
    hue - переменная для цветового кодирования (если system=True)
    size - переменная кодирования по размеру (если system=True)
    sizes - переменная для уточнения диапозона кодировки размеров (если system=True)
    system - флаг для построения scatter-графика
    discrete - булевое значение, дискретные значение или нет.
    log_scale - логарифмическая шкала для гистограммы
    title_hist - заголовок для гистограммы и boxplot
    title_scatter - заголовок для scatter-графика
    '''

    # Названия для графиков по умолчанию
    if title_hist is None:
        title_hist = f"Распределение {df.name if hasattr(df, 'name') else 'входной признак'}"
    if title_scatter is None:
        target_name = target.name if hasattr(target, 'name') else 'таргета'
        feature_name = df.name if hasattr(df, 'name') else 'входной признак'
        title_scatter = f"Зависимость {target_name} от {feature_name}"
    
    # Создание составного графика: boxplot + histogram
    fig, (ax_box, ax_hist) = plt.subplots(2, sharex=True, 
                                          figsize=(8, 5.6), 
                                          gridspec_kw={'height_ratios': (.15, .85)})
    
    # Boxplot
    sns.boxplot(x=df, orient='h', ax=ax_box)
    ax_box.set(xlabel='')

    # Histogram
    n_bins = round(1 + math.log2(len(df))) if len(df) > 1 else 10
    sns.histplot(x=df, bins=n_bins, discrete=discrete, log_scale=log_scale, ax=ax_hist)

    # Общие настройки для основной фигуры
    ax_hist.set_xlabel(x_label, fontsize=10)
    ax_hist.set_ylabel(y_label, fontsize=10)
    ax_hist.tick_params(axis='both', which='major', labelsize=10)
    fig.suptitle(title_hist, fontsize=12, fontweight='bold', y=0.95)

    # Настройка тиков для дискретных значений
    if discrete == 'unique':
        unique_vals = np.sort(df.dropna().unique())
        ax_hist.set_xticks(unique_vals)
    elif discrete == 'non_unique':
        ax_hist.set_xticks(np.arange(df.min(), df.max() + 1, 1))

    # Отображение первой фигуры
    fig.tight_layout()

    # Scatter plot (если запрошено)
    if system:
        if hue is not None:
            fig_2, ax_2 = plt.subplots(figsize=(10, 5.34))
        else:
            fig_2, ax_2 = plt.subplots(figsize=(8, 5.34))
        sns.scatterplot(x=df, y=target, alpha=0.5, hue=hue, size=size, sizes=sizes, ax=ax_2)
        fig_2.suptitle(title_scatter, fontsize=12, fontweight='bold', y=0.95)
        ax_2.set_xlabel(x_label, fontsize=10)
        ax_2.set_ylabel(target.name if hasattr(target, 'name') else 'Целевая переменная', fontsize=10)
        ax_2.tick_params(axis='both', which='major', labelsize=10)
        if hue is not None:
            ax_2.legend(bbox_to_anchor=(1.025, 1), loc='upper left', fontsize=10)
        fig_2.tight_layout()
        if log_scale:
            plt.xscale('log')
            plt.yscale('log')

    # Отображение графика
    plt.show()

    # Вывод статистических метрик
    display(df.describe().to_frame().T)
        
    # Проверяем нормальность распределения
    result = anderson(df.dropna())
    
    if result.statistic < result.critical_values[2]:
        distr = 'Нормальное'
    else:
        distr = 'Не является нормальным'

    test_anderson = {'':['Статистика:', 
                         "Критические значения:", 
                         'Распределение'], 
                     'Тест на нормальность распределения (порог=0.05):': [result.statistic, 
                                                                          result.critical_values, 
                                                                          distr]
                    }
    display(pd.DataFrame(test_anderson).set_index(''))
In [5]:
# Функция для анализа категорийных значений

def analyzis_category(df, title=None, kind='bar'):
    '''
    Данная функция выводит столбчатый график
    по указанному столбцу датафрейма и его значения 
    в табличном виде.
    Аргументы функции:
    df - данные
    name - название графика
    kind - ориентация графика вертикальная или 
    горизонтальная, принимает значения "bar" и "barh".
    '''
    # Название для графика по умолчанию
    if title is None:
        title = f"Соотношение категорий {df.name if hasattr(df, 'name') else 'входного признака'}"
    
    # Подсчитываем количество каждого значения
    category_count = df.value_counts(ascending=True)

    # Создание столбчатого графика
    plot_bar = category_count.plot(kind=kind, figsize=(8, 5.6), grid=True)

    # Настройка заголовка и подписей
    if kind == 'bar':
        plt.title(title, fontweight='bold', fontsize=12)
        plt.xlabel('')
        plt.ylabel('Частота', fontsize=10)
        plt.tick_params(axis='both', which='major', labelsize=10)
        plt.xticks(rotation=0)
    
    else:
        plt.title(title, fontweight='bold', fontsize=12)
        plt.xlabel('Частота', fontsize=10)
        plt.ylabel('')
        plt.tick_params(axis='both', which='major', labelsize=10)
        plt.xticks(rotation=0)

    # Отображаем график
    plt.tight_layout()
    plt.show()

    # Вывод значений в табличном виде
    display(pd.DataFrame(category_count).reset_index())
In [6]:
# Построение таблицы с расчитанным VIF
def vif_factor(data, columns):
    '''
    Данная функция расчитываем VIF-фактор
    для количественных признаков датафрейма.
    На ввод функция принимает:
    data - датафрейм
    columns - список количественных признаков.
    '''
    vif_data = pd.DataFrame()
    vif_data['input attribute'] = columns
    vif_data['vif'] = ([variance_inflation_factor(data[columns].values, i) 
                        for i in range(data[columns].shape[1])])
    return vif_data
In [7]:
# Функция для подготовки данных к обучению модели
def add_feature(X):
    X = X.copy()
    X['log_luminosity'] = np.log(X['luminosity'])
    X['log_radius'] = np.log(X['radius'])
    X['ica_abs_magnitude'] = X['abs_magnitude']
    return X
In [8]:
# Функция для расчета эффективной температуры с помощью закона Стефана–Больцмана
def baseline_bf(X, y):
    
    L_0 = 3.828 * 10**26
    R_0 = 6.9551 * 10**8
    sigma = 5.67 * 10**(-8)
    
    X = X.copy()
    T = ((X['luminosity'] * L_0) / (4 * np.pi * (X['radius'] * R_0)**2 * sigma))**(0.25)
    
    rmse_score = np.sqrt(mean_squared_error(y, T))
    
    print(f'Значение метрики RMSE: {rmse_score:.3f}')
    
    return rmse_score
In [9]:
# Функция построения графика для анализа результатов прогноза
def analyzis_test(y_true, y_pred):
        
        # Объединение данных в один датафрейм
        y_pred = pd.Series(y_pred, index=y_true.index, name='pred')
        df = pd.concat([y_true.rename('true'), y_pred], axis=1)\
               .reset_index()
        
        # Длинный формат таблицы
        df = df.melt(id_vars='index', 
                     var_name='type', 
                     value_name='temperature')
        
        # Построение графика
        plt.figure(figsize=(15, 5.6))
        sns.barplot(data=df, 
                    x='index', 
                    y='temperature', 
                    hue='type')
        
        # Настройка заголовка и подписей
        plt.title('Анализ прогноза модели', fontweight='bold', fontsize=12)
        plt.xlabel('Номер звезды в данных', fontsize=10)
        plt.ylabel('Температура звезды', fontsize=10)
        
        # Вывод графика
        plt.tight_layout()
        plt.show()
In [10]:
# Функция для установление весов по умолчанию
def init_weights(layer):
    if isinstance(layer, nn.Linear):
        nn.init.xavier_uniform_(layer.weight)
        if layer.bias is not None:
            nn.init.zeros_(layer.bias)

Общая информация о данных¶

Загрузка данных:¶

In [11]:
# Загрузка данных
df = pd.read_csv('../data/6_class.csv', index_col=0)

Общая информация о датафрейме:¶

Описание данных:

6_class.csv — описание уже изученных звезд.

Описание признаков:

  • Luminosity(L/Lo) — светимость звезды относительно Солнца;
  • Radius(R/Ro) — радиус звезды относительно радиуса Солнца;
  • Absolute magnitude(Mv) — абсолютная звёздная величина, физическая величина, характеризующая блеск звезды;
  • Star color — (white, red, blue, yellow, yellow-orange и др.) — цвет звезды, который определяют на основе спектрального анализа;
  • Star type — тип звезды (описание типов в таблице ниже);
Тип звезды Номер, соответствующий типу
Коричневый карлик 0
Красный карлик 1
Белый карлик 2
Звёзды главной последовательности 3
Сверхгигант 4
Гипергигант 5
  • Temperature (K)— (Целевой признак) — абсолютная температура на поверхности звезды в Кельвинах.
In [12]:
gen_info(df)
<class 'pandas.core.frame.DataFrame'>
Index: 240 entries, 0 to 239
Data columns (total 6 columns):
 #   Column                  Non-Null Count  Dtype  
---  ------                  --------------  -----  
 0   Temperature (K)         240 non-null    int64  
 1   Luminosity(L/Lo)        240 non-null    float64
 2   Radius(R/Ro)            240 non-null    float64
 3   Absolute magnitude(Mv)  240 non-null    float64
 4   Star type               240 non-null    int64  
 5   Star color              240 non-null    object 
dtypes: float64(3), int64(2), object(1)
memory usage: 13.1+ KB
None
count mean std min 25% 50% 75% max
Temperature (K) 240.0 10497.462500 9552.425037 1939.00000 3344.250000 5776.0000 15055.5000 40000.00
Luminosity(L/Lo) 240.0 107188.361635 179432.244940 0.00008 0.000865 0.0705 198050.0000 849420.00
Radius(R/Ro) 240.0 237.157781 517.155763 0.00840 0.102750 0.7625 42.7500 1948.50
Absolute magnitude(Mv) 240.0 4.382396 10.532512 -11.92000 -6.232500 8.3130 13.6975 20.06
Star type 240.0 2.500000 1.711394 0.00000 1.000000 2.5000 4.0000 5.00
Temperature (K) Luminosity(L/Lo) Radius(R/Ro) Absolute magnitude(Mv) Star type Star color
150 29560 188000.00000 6.0200 -4.01 3 Blue-white
127 2861 0.00019 0.0899 16.71 0 Red
191 3257 0.00240 0.4600 10.73 1 Red
28 11790 0.00015 0.0110 12.59 2 Yellowish White
165 7282 131000.00000 24.0000 -7.22 4 Blue

Вывод:¶

Получив общую информацию о датафрейме, можем сделать следующие выводы и построить предварительный план по предобработке данных:

  • Нужно поменять названия столбцов для удобства работы;
  • Пропуски отсутствуют;
  • Типы данных корректны;
  • Необходимо проверить данные на явные и неявные дубликаты.

Предобработка данных¶

Переименование названия столбцов:¶

In [13]:
df = df.rename(columns={'Temperature (K)': 'temperature',
                        'Luminosity(L/Lo)': 'luminosity',
                        'Radius(R/Ro)': 'radius',
                        'Absolute magnitude(Mv)': 'abs_magnitude',
                        'Star type': 'star_type',
                        'Star color': 'star_color'})

Неявные дубликаты:¶

In [14]:
hidden_dup_search(df)
Уникальные значения признака: star_color
['red' 'blue_white' 'white' 'yellowish_white' 'pale_yellow_orange' 'blue'
 'whitish' 'yellow_white' 'orange' 'white_yellow' 'blue_' 'yellowish'
 'orange_red' 'blue_white_']

Мы получили 14 уникальных значений, когда звезды по цвету и температуре делят на 7 категорий:

Классификация звезд

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

In [15]:
# Фильтрация по неоднозначным цветам звезд
df[df['star_color'].isin(['whitish', 'yellowish', 'orange_red', 'pale_yellow_orange'])]
Out[15]:
temperature luminosity radius abs_magnitude star_type star_color
29 7230 0.00008 0.013 14.080 2 pale_yellow_orange
33 9700 74.00000 2.890 0.160 3 whitish
35 8052 8.70000 1.800 2.420 3 whitish
91 4526 0.15300 0.865 6.506 3 yellowish
92 4077 0.08500 0.795 6.228 3 yellowish
93 4980 0.35700 1.130 4.780 3 yellowish
96 5112 0.63000 0.876 4.680 3 orange_red

Судя по всему эти названия видимых цветов звезд, заменим их соответствующий им истинный.

  • pale_yellow_orange — определяем, как оранжевый цвет;
  • whitish — определили, как белый;
  • yellowish — определили, как желтый;
  • orange_red — определили, как красный.
In [16]:
# Карта замены значений
mapping = {'yellowish_white': 'yellow_white',
           'pale_yellow_orange': 'orange',
           'whitish': 'white',
           'white_yellow': 'yellow_white', 
           'blue_': 'blue', 
           'yellowish': 'yellow',
           'orange_red': 'red',
           'blue_white_': 'blue_white'}

# Замена значений
df['star_color'] = df['star_color'].replace(mapping)

# Проверка замены
print(f'Кол-во уникальных значений: {df["star_color"].nunique()}')
print(f'Уникальные значения: {df["star_color"].unique().tolist()}')
Кол-во уникальных значений: 7
Уникальные значения: ['red', 'blue_white', 'white', 'yellow_white', 'orange', 'blue', 'yellow']

Теперь наши значения приведены к стандарту и избавлены от неявных дубликатов.

Явные дубликаты:¶

In [17]:
# Количество явных дубликатов
df.duplicated().sum()
Out[17]:
np.int64(0)

Явные дубликаты отсутствуют.

Выводы:¶

  • Названия колонок приведены к snake виду;
  • Исправлены неявные дубликаты в признаке star_color;
  • Явные дубликаты отсутствуют.

Исследовательский анализ¶

Количественные признаки:¶

In [18]:
# Список количественных признаков
num_col = ['temperature',
           'luminosity', 
           'radius', 
           'abs_magnitude']
In [19]:
# Статистическое описание количественных признаков
df[num_col].describe().T
Out[19]:
count mean std min 25% 50% 75% max
temperature 240.0 10497.462500 9552.425037 1939.00000 3344.250000 5776.0000 15055.5000 40000.00
luminosity 240.0 107188.361635 179432.244940 0.00008 0.000865 0.0705 198050.0000 849420.00
radius 240.0 237.157781 517.155763 0.00840 0.102750 0.7625 42.7500 1948.50
abs_magnitude 240.0 4.382396 10.532512 -11.92000 -6.232500 8.3130 13.6975 20.06

По статистие количественных признаков можем видеть, что у первых 3х признаков положительно скошенное распределение. Аномалий в значениях не наблюдаем. Рассмотрим признаки более подробно.

Temperature (таргет):¶

In [20]:
analyzis_quantity(df['temperature'], x_label='Температура звезды в Кельвинах (Таргет)')
No description has been provided for this image
count mean std min 25% 50% 75% max
temperature 240.0 10497.4625 9552.425037 1939.0 3344.25 5776.0 15055.5 40000.0
Тест на нормальность распределения (порог=0.05):
Статистика: 18.211866
Критические значения: [0.567, 0.646, 0.774, 0.903, 1.075]
Распределение Не является нормальным

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

Luminosity¶

In [21]:
analyzis_quantity(df['luminosity'], x_label='Относительная светимость звезды', 
                  system=True, target=df['temperature'], hue=df['star_color'], 
                  size=df['star_type'], sizes=(20, 100), log_scale=True)
No description has been provided for this image
No description has been provided for this image
count mean std min 25% 50% 75% max
luminosity 240.0 107188.361635 179432.24494 0.00008 0.000865 0.0705 198050.0 849420.0
Тест на нормальность распределения (порог=0.05):
Статистика: 30.961228
Критические значения: [0.567, 0.646, 0.774, 0.903, 1.075]
Распределение Не является нормальным
  • Видим зависимость, что чем больше светимость звезды, тем она больше по размеру. Признак имеет бимодальное распределение с пиками на значениях $10^{-3}$ и второй пик на значениях более $10^5$. Наблюдаем малое количество объектов с средней светимостью.
  • Голубые звезды имеют очень сильный разброс по температуре. Данную картину можно более красочно рассмотреть тут:

Светимость vs Температура vs Размер vs Цвет

Radius¶

In [22]:
analyzis_quantity(df['radius'], x_label='Относительный радиус звезды', 
                  system=True, target=df['temperature'], hue=df['star_color'], 
                  size=df['star_type'], sizes=(20, 100), log_scale=True)
No description has been provided for this image
No description has been provided for this image
count mean std min 25% 50% 75% max
radius 240.0 237.157781 517.155763 0.0084 0.10275 0.7625 42.75 1948.5
Тест на нормальность распределения (порог=0.05):
Статистика: 59.646879
Критические значения: [0.567, 0.646, 0.774, 0.903, 1.075]
Распределение Не является нормальным

Мы получили положительно скошенное распределение с несколькими пиками приблизительно на каждой степени 10 и отдельно стоящий пик на значении $10^3$.

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

Abs_magnitude¶

In [23]:
analyzis_quantity(df['abs_magnitude'], x_label='Абсолютная величина звезды', 
                  system=True, target=df['temperature'], hue=df['star_color'], 
                  size=df['star_type'], sizes=(20, 100))
No description has been provided for this image
No description has been provided for this image
count mean std min 25% 50% 75% max
abs_magnitude 240.0 4.382396 10.532512 -11.92 -6.2325 8.313 13.6975 20.06
Тест на нормальность распределения (порог=0.05):
Статистика: 13.109089
Критические значения: [0.567, 0.646, 0.774, 0.903, 1.075]
Распределение Не является нормальным

Признак имеет бимодальное распределение, с пиками на значениях ~(-7) и ~12. В основном все значения имеют колоколобразную зависимость с таргетом, кроме кластера звезд с абсолютной величиной от 10 до 15 и имеют тип 2 - 3 там зависимость линейная.

Категориальные признаки:¶

Star_type¶

In [24]:
analyzis_category(df['star_type'])
No description has been provided for this image
star_type count
0 0 40
1 1 40
2 2 40
3 3 40
4 4 40
5 5 40
In [25]:
display(df.groupby('star_type', as_index=False)
          .agg(temperature_mean=('temperature', 'mean'),
               temperature_median=('temperature', 'median'))
          .sort_values(by='temperature_mean', ascending=False))
star_type temperature_mean temperature_median
3 3 16018.000 12560.5
4 4 15347.850 12821.0
2 2 13931.450 13380.0
5 5 11405.700 3766.0
1 1 3283.825 3314.0
0 0 2997.950 2935.0

Типы звезд равномерно распределены по 40 штук в каждом типе. Начиная со 2го типа звезды средняя и медианная температура резко возрастает и начинает снижатся только на 5 типе звезды.

На 2 - 3 типах звезд а нас есть по другим признаками либо экспоненциальная либо линейная зависимость, наиболе сложная зона для прогнозирования на данный момент выглядят звезды с 4 и 5 типами у них слишком большой разброс по уровню температуры.

Star_color¶

In [26]:
analyzis_category(df['star_color'])
No description has been provided for this image
star_color count
0 orange 3
1 yellow 3
2 white 12
3 yellow_white 12
4 blue_white 41
5 blue 56
6 red 113
In [27]:
display(df.groupby('star_color', as_index=False)
          .agg(temperature_mean=('temperature', 'mean'), 
               temperature_median=('temperature', 'median'))
          .sort_values(by='temperature_mean', ascending=False))
star_color temperature_mean temperature_median
0 blue 21918.339286 19921.5
1 blue_white 16659.951220 14100.0
4 white 9579.583333 8879.5
6 yellow_white 7609.166667 6928.5
2 orange 5088.666667 4287.0
5 yellow 4527.666667 4526.0
3 red 3307.893805 3324.0

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

Данный дисбаланс стоит учитывать при делении данных на тестовую и тренировочную выборки.

Вывод:¶

Temperature

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

Luminosity

Видим зависимость, что чем больше светимость звезды, тем она больше по размеру. Признак имеет бимодальное распределение с пиками на значениях $10^{-3}$ и второй пик на значениях более $10^5$. Наблюдаем малое количество объектов с средней светимостью. Голубые звезды имеют очень сильный разброс по температуре.

Radius

Мы получили положительно скошенное распределение с несколькими пиками приблизительно на каждой степени 10 и отдельно стоящий пик на значении $10^3$.

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

Abs_magnitude

Признак имеет бимодальное распределение, с пиками на значениях ~(-7) и ~12. В основном все значения имеют колоколобразную зависимость с таргетом, кроме кластера звезд с абсолютной величиной от 10 до 15 и имеют тип 2 - 3 там зависимость линейная.

Star_type

Типы звезд равномерно распределены по 40 штук в каждом типе. Начиная со 2го типа звезды средняя и медианная температура резко возрастает и начинает снижатся только на 5 типе звезды.

На 2 - 3 типах звезд а нас есть по другим признаками либо экспоненциальная либо линейная зависимость, наиболе сложная зона для прогнозирования на данный момент выглядят звезды с 4 и 5 типами у них слишком большой разброс по уровню температуры.

Star_color

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

Данный дисбаланс стоит учитывать при делении данных на тестовую и тренировочную выборки.

Общее наблюдение

В признаках Temperature, Luminosity и Radius теряется информация при разных взглядах на значения (Логариффмирование значений: да/нет), необходимо продумать методы, для передачи информации модели, так как при оригинальных значениях мы хорошо видим картину для высоких значений признака, и плохо для малых, и наоборот при логарифмировании.

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

Корреляционный анализ¶

Матрица корредяции¶

In [28]:
# Список количественных признаков
num_cols = ['temperature', 
            'luminosity', 
            'radius', 
            'abs_magnitude']

Метод Пирсона¶

In [29]:
# Построение матрица корреляции
fig, ax = plt.subplots(figsize=(6, 5))
sns.heatmap(df[num_cols].corr(), annot=True, 
            fmt='.3f', linewidths=.5, cmap='cividis', ax=ax)

# Настройка заголовка и подписей
plt.title('Матрица корреляции для количественных признаков', fontweight='bold', fontsize=12)
plt.xticks(rotation=0)
plt.yticks(rotation=0)

# Отображаем график
plt.tight_layout()
plt.show()
No description has been provided for this image

Метод Спирмена¶

In [30]:
# Построение матрица корреляции
fig, ax = plt.subplots(figsize=(6, 5))
sns.heatmap(df[num_cols].corr(method='spearman'), annot=True, 
            fmt='.3f', linewidths=.5, cmap='cividis', ax=ax)

# Настройка заголовка и подписей
plt.title('Матрица корреляции для количественных признаков', fontweight='bold', fontsize=12)
plt.xticks(rotation=0)
plt.yticks(rotation=0)

# Отображаем график
plt.tight_layout()
plt.show()
No description has been provided for this image

Все сильные завивисимости довольно логичны и объяснимы, по формулам:

Абсолютная визуальная звёздная величина:

$$ M_V = -2.5 \cdot \log_{10} \left( \frac{L_V}{L_{V,\odot}} \right) + M_{V,\odot} $$

где:

  • ($M_V$) — абсолютная визуальная величина звезды,
  • ($L_V$) — светимость звезды в визуальном диапазоне,
  • ($L_{V,\odot}$) — светимость Солнца в том же диапазоне (≈ 3.828 × 10²⁶ Вт),
  • ($M_{V,\odot}$) — абсолютная визуальная величина Солнца (≈ 4.83).

Светимость через радиус и температуру (закон Стефана–Больцмана):

$$ L = 4 \pi R^2 \sigma T^4 $$

где:

  • ($L$) — полная светимость звезды,
  • ($R$) — радиус звезды,
  • ($T$) — эффективная температура звезды (в Кельвинах),
  • ($\sigma$) — постоянная Стефана–Больцмана (≈ 5.67 × 10⁻⁸ Вт·м⁻²·К⁻⁴).

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

Матрица Phik¶

Рассмотрим зависимость признаков с помощью матрицы Phik.

In [31]:
# Строим тепловую карту
f, ax = plt.subplots(figsize=(9, 7))
sns.heatmap(df.phik_matrix(interval_cols=num_cols), annot=True, 
            fmt='.2f', linewidths=.5, cmap='cividis', ax=ax)

# Настройка заголовка и подписей
plt.title('Матрица Phik для анализа взаимосвязей признаков', fontweight='bold', fontsize=12)
plt.yticks(rotation=0)

# Отображаем график
plt.tight_layout()
plt.show()
No description has been provided for this image

Вывод:¶

Все сильные завивисимости довольно логичны и объяснимы, по формулам:

Абсолютная визуальная звёздная величина:

$$ M_V = -2.5 \cdot \log_{10} \left( \frac{L_V}{L_{V,\odot}} \right) + M_{V,\odot} $$

где:

  • ($M_V$) — абсолютная визуальная величина звезды,
  • ($L_V$) — светимость звезды в визуальном диапазоне,
  • ($L_{V,\odot}$) — светимость Солнца в том же диапазоне (≈ 3.828 × 10²⁶ Вт),
  • ($M_{V,\odot}$) — абсолютная визуальная величина Солнца (≈ 4.83).

Светимость через радиус и температуру (закон Стефана–Больцмана):

$$ L = 4 \pi R^2 \sigma T^4 $$

где:

  • ($L$) — полная светимость звезды,
  • ($R$) — радиус звезды,
  • ($T$) — эффективная температура звезды (в Кельвинах),
  • ($\sigma$) — постоянная Стефана–Больцмана (≈ 5.67 × 10⁻⁸ Вт·м⁻²·К⁻⁴).

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

Тип звезды и абсолютная величина дают значение 0.92, так как для классификации типа звезды используются значения второго признака, но это не единственный компонент, поэтому они друг друга не дублируют.

Наибольшую зависимость с таргетом показали признаки star_color и abs_magnitude — 0.71.

В виду сильной зависимости между входными признаками возможно будет полезно применить к признакам метод независимых компонент.

Подготовка данных к обучению модели¶

Деление данных на тренировочную и тестовые выборки:¶

In [32]:
# Входные признаки
X = df.drop('temperature', axis=1)

# Целевая переменная
y = df['temperature']
In [33]:
# Деление на train и test
X_train, X_test, y_train, y_test = train_test_split(
    X, 
    y, 
    test_size=0.25, 
    random_state=RANDOM_STATE,
    stratify=pd.qcut(y, q=10, duplicates='drop')
)
In [34]:
# Проверяем размеры выборок
print(f'Размер обучающей выборки: {X_train.shape, y_train.shape}')
print(f'Размер тестовой выборки: {X_test.shape, y_test.shape}')
Размер обучающей выборки: ((180, 5), (180,))
Размер тестовой выборки: ((60, 5), (60,))

Данные разделены корректно.

Feature Engineering:¶

Поскольку признаки в данных частично линейно зависимы (например, абсолютная величина $M_V$ и светимость $L$ взаимно определяются через расстояние), их прямое использование может привести к избыточности и неэффективному обучению нейросети. Для повышения устойчивости модели и улучшения сходимости мы применим преобразования — логарифмирование и метод независимых компонент (FastICA) — чтобы выделить уникальную информацию, скрытую за коррелированными входными признаками.

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

In [35]:
# Дата для отбора признаков
selection = X_train.copy()

Логарифмирование признаков¶

In [36]:
selection['radius_log'] = np.log(selection['radius'])
selection['luminosity_log'] = np.log(selection['luminosity'])
In [37]:
log_y_train = np.log(y_train)

FastICA¶

Получим независимые компоненты для оригинальных и для преобразованных признаков.

In [38]:
# Для оригинальных признаков
ica_1 = FastICA(n_components=3, random_state=RANDOM_STATE)

# Для логарифмированных признаков
ica_2 = FastICA(n_components=3, random_state=RANDOM_STATE)
In [39]:
# Компоненты для преобразования
raw_feature = selection[['luminosity', 'radius', 'abs_magnitude',]]
log_feature = selection[['luminosity_log', 'radius_log', 'abs_magnitude',]]
In [40]:
# Оригинальные признаки
ica_raw = ica_1.fit_transform(raw_feature)

# Логарифмированные признаки
ica_log = ica_2.fit_transform(log_feature)
In [41]:
# Добравляем признаки в датафрейм
for i in range(3):
    selection['ica_raw_' + str(i)] = ica_raw[:, i]
    selection['ica_log_' + str(i)] = ica_log[:, i]

Mi-scores¶

Оценим признаки с помощью mutual_info_regression для выбора оптимального набора признаков для обучения модели.

In [42]:
# Список количественных признаков
mi_feature = selection.columns.to_list()
mi_feature.remove('star_type')
mi_feature.remove('star_color')
In [43]:
# Считает значения MI
mi_scores = mutual_info_regression(selection[mi_feature], log_y_train, random_state=RANDOM_STATE)

# Записываем значения в Series
mi_scores = pd.Series(mi_scores, index=selection[mi_feature].columns).sort_values(ascending=False)

# Выводим рейтинг
mi_scores
Out[43]:
radius_log        0.972933
abs_magnitude     0.851626
ica_log_1         0.814257
luminosity_log    0.723577
ica_log_2         0.660272
ica_raw_2         0.587739
ica_raw_1         0.536323
ica_raw_0         0.365323
ica_log_0         0.265843
radius            0.189478
luminosity        0.188866
dtype: float64

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

Максимальную информацию несет логарифмированный набор признаков, посмотрим с помощью VIF-фактора, а можем ли мы взять что то из независимых компонент.

In [44]:
vif_feature = ['radius_log', 
               'abs_magnitude', 
               'luminosity_log',
               'ica_raw_1']
In [45]:
vif_factor(selection, vif_feature)
Out[45]:
input attribute vif
0 radius_log 6.919295
1 abs_magnitude 3.115961
2 luminosity_log 8.566673
3 ica_raw_1 1.193582

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

Построение пайплайна для подготовки данных к обучению модели:¶

In [46]:
# Списки необходимые для преобразования данных
ica_feature = ['luminosity', 'radius', 'ica_abs_magnitude']
scaler_feature = ['ica__fastica1',
                  'remainder__abs_magnitude',
                  'remainder__log_luminosity',
                  'remainder__log_radius']
ohe_feature = ['remainder__star_type', 'remainder__star_color']
In [47]:
# Получаем логарифмированные признаки
log_transformer = FunctionTransformer(add_feature, validate=False).set_output(transform='pandas')
In [48]:
# Получаем независимые компоненты
ica_transformer = ColumnTransformer([
    
    ('ica', FastICA(n_components=3, random_state=RANDOM_STATE), ica_feature)
    
], remainder='passthrough').set_output(transform='pandas')
In [49]:
# Масштабируем и кодируем данные
scale_transformer = ColumnTransformer([
    
    ('num', StandardScaler(), scaler_feature), 
    
    ('ohe', OneHotEncoder(handle_unknown='ignore', 
                          drop='first', 
                          sparse_output=False), ohe_feature)
    
], remainder='drop').set_output(transform='pandas')
In [50]:
# Преобразование данных в тензор
tensor_transformer = FunctionTransformer(lambda x: torch.FloatTensor(x.values), validate=False)
In [51]:
# Финальный пайплайн для подготовки данных с обычным масштабированием
processor = Pipeline([
    ('log_feature', log_transformer), 
    ('ica', ica_transformer),
    ('scale', scale_transformer),
    ('tensor', tensor_transformer)
])

Преобразование данных для обучения модели:¶

In [52]:
# Тренировочные данные
X_train_scale = processor.fit_transform(X_train)

# Целевая переменная тренировочных данных
y_train_tensor = torch.FloatTensor(np.log(y_train).values)

# Тестовые данные
X_test_scale = processor.transform(X_test)

# Целевая переменная тестовых данных
y_test_numpy = y_test.values

Вывод:¶

В данном разделе мы выполнили следующие шаги:

  • Мы разделили данные на тренировочные и тестовые выборки;
  • Применили к признакам преобразования для увеличивания в них информации, необходимой для прогнозирования целевого признака;
  • Отобрали признаки для обучения с помощью mutual_info_regression;
  • Построили пайплайны подготовки данных для предотвращения утечки данных.

Построение базовой нейронной сети¶

Baseline:¶

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

В качестве формулы расчета мы выбрали именно этот метод, так как у нас есть все необходимые переменные, чтобы по ней сделать расчет.

Выразим из этой формулы температуру:

$$ L = 4 \pi R^2 \sigma T^4 $$

где:

  • ($L$) — полная светимость звезды,
  • ($R$) — радиус звезды,
  • ($T$) — эффективная температура звезды (в Кельвинах),
  • ($\sigma$) — постоянная Стефана–Больцмана (≈ 5.67 × 10⁻⁸ Вт·м⁻²·К⁻⁴).

$$ T = \left( \frac{L}{4 \pi R^2 \sigma} \right)^{1/4} $$

Чтобы привести наши значения из относительных в абсолютные значения, будем использовать справочную информацию, данную перед началом работы:

Светимость Солнца — $L_{\odot} = 3.828⋅10^{26}$

Радиус Солнца — $R_{\odot} = 6.9551⋅10^8$

In [53]:
rmse_bf = baseline_bf(X_test, y_test)
Значение метрики RMSE: 7646.102

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

Baseline Neural Network:¶

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

Инициализация моделей¶

In [54]:
# Инициализация класса модели
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        
        # Слои модели
        self.fc1 = nn.Linear(15, 128)
        self.act1 = nn.GELU()
        
        self.fc2 = nn.Linear(128, 128)
        self.act2 = nn.GELU()
        
        self.fc3 = nn.Linear(128, 128)
        self.act3 = nn.GELU()
        
        self.fc4 = nn.Linear(128, 1)
    
    # Прямое прохождение сети
    def forward(self, x):
        
        x = self.fc1(x)
        x = self.act1(x)
        
        x = self.fc2(x)
        x = self.act2(x)
        
        x = self.fc3(x)
        x = self.act3(x)
        
        x = self.fc4(x)
        
        return x
In [55]:
# Инициализация модели
net = Net()
optimizer = torch.optim.Adam(net.parameters(), lr=0.001, weight_decay=1e-6)
loss = nn.MSELoss()
num_epochs = 3000

# Инициализация весов
net.apply(init_weights)
Out[55]:
Net(
  (fc1): Linear(in_features=15, out_features=128, bias=True)
  (act1): GELU(approximate='none')
  (fc2): Linear(in_features=128, out_features=128, bias=True)
  (act2): GELU(approximate='none')
  (fc3): Linear(in_features=128, out_features=128, bias=True)
  (act3): GELU(approximate='none')
  (fc4): Linear(in_features=128, out_features=1, bias=True)
)

Обучение модели¶

In [56]:
# Параметры обучения
patience = 100
min_delta = 1e-6
best_rmse = float('inf')
patience_count = 0
baseline_pred = None
baseline_rmse = None

# Обучение модели
for epoch in range(num_epochs):
    
    net.train()
    
    optimizer.zero_grad()
    preds = net(X_train_scale).flatten()
    
    loss_value = loss(preds, y_train_tensor)
    loss_value.backward()
    
    optimizer.step()
    
    # Оценка модели
    net.eval()
        
    with torch.no_grad():
            
        baseline_pred = net(X_test_scale).flatten()
        baseline_pred = np.exp(baseline_pred.numpy())
        baseline_rmse = np.sqrt(mean_squared_error(y_test_numpy, baseline_pred))
        
    # Ранняя остановка
    if baseline_rmse < best_rmse - min_delta:
        best_rmse = baseline_rmse
        patience_count = 0
    else:
        patience_count += 1
        if patience_count >= patience:
            print(f"Early stopping at epoch {epoch}, final RMSE: {baseline_rmse:.6f}")
            break
    
    # Результаты обучения
    if epoch % 100 == 0 or epoch == num_epochs - 1:
        print(f"Epoch {epoch}, RMSE: {baseline_rmse:.6f}")
Epoch 0, RMSE: 13818.478643
Epoch 100, RMSE: 11332.816773
Epoch 200, RMSE: 7864.731909
Epoch 300, RMSE: 5095.715651
Epoch 400, RMSE: 4187.491851
Epoch 500, RMSE: 4004.529810
Epoch 600, RMSE: 3914.418092
Epoch 700, RMSE: 3814.133716
Epoch 800, RMSE: 3724.264894
Epoch 900, RMSE: 3659.499283
Epoch 1000, RMSE: 3615.355308
Epoch 1100, RMSE: 3583.280759
Epoch 1200, RMSE: 3555.792598
Epoch 1300, RMSE: 3528.689133
Epoch 1400, RMSE: 3501.230784
Epoch 1500, RMSE: 3474.095422
Epoch 1600, RMSE: 3447.845849
Epoch 1700, RMSE: 3422.896580
Epoch 1800, RMSE: 3399.826760
Epoch 1900, RMSE: 3379.878400
Epoch 2000, RMSE: 3364.868645
Epoch 2100, RMSE: 3356.879354
Epoch 2200, RMSE: 3357.439054
Early stopping at epoch 2248, final RMSE: 3360.928889

Анализ результатов¶

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

In [57]:
analyzis_test(y_test, baseline_pred)
No description has been provided for this image

В целом модель довольно хорошо справляется, проблема только в нескольких звездах, где ошибка очень велика, рассмотрим эти звезды в таблице.

In [58]:
# Список индексов интересующих нас звезд
idx_star_false = [81, 102, 105, 118, 141, 162, 163, 165, 173, 177, 178, 214, 215]

# Фильтруем таблицу, оставляя только интересующия нас звезды
false_predict = df.loc[df.index.isin(idx_star_false)]

false_predict
Out[58]:
temperature luminosity radius abs_magnitude star_type star_color
81 10574 0.00014 0.0092 12.02 2 white
102 23000 127000.00000 36.0000 -5.76 4 blue
105 14245 231000.00000 42.0000 -6.12 4 blue
118 6850 229000.00000 1467.0000 -10.07 5 red
141 21020 0.00150 0.0112 11.52 2 blue
162 12675 452000.00000 83.0000 -5.62 4 blue
163 5752 245000.00000 97.0000 -6.63 4 blue
165 7282 131000.00000 24.0000 -7.22 4 blue
173 26000 316000.00000 1679.0000 -9.10 5 blue
177 11000 170000.00000 1779.0000 -9.90 5 blue_white
178 12100 120000.00000 708.9000 -7.84 5 blue_white
214 34190 198200.00000 6.3900 -4.57 3 blue
215 32460 173800.00000 6.2370 -4.36 3 blue

По этим данным мы видим, что у модели сложности с голубыми звездами 4 и 5 типа, попробуем улучшить модель с помощью регуляризации и добавлением батчей. Если данный подход не поможет, прибегнем к аугментации данных.

Вывод:¶

На данном этапе работы мы получили базовые метрики качества моделей, проанализировали полученные результаты.

Метрики качества RMSE:

  • Закон Стефана-Больцмана — 7646.102
  • Базовая нейросеть — ~3360.929

Проанализировав прогнозы модели на тестовой выборке, сделали следующие выводы, что у модели сложности с голубыми звездами 4 и 5 типа, попробуем улучшить модель с помощью регуляризации и добавлением батчей. Если данный подход не поможет, прибегнем к аугментации данных.

Улучшение нейронной сети¶

In [59]:
# Класс улучшенной нейросети
class Net_2(nn.Module):
    def __init__(self, dropout_rate=0.01):
        super(Net_2, self).__init__()
        # Слои модели
        self.fc1 = nn.Linear(15, 128)
        self.bn1 = nn.BatchNorm1d(128)
        self.act1 = nn.GELU()
        self.drop1 = nn.Dropout(dropout_rate)
                
        self.fc2 = nn.Linear(128, 128)
        self.bn2 = nn.BatchNorm1d(128)
        self.act2 = nn.GELU()
        self.drop2 = nn.Dropout(dropout_rate)
                
        self.fc3 = nn.Linear(128, 128)
        self.bn3 = nn.BatchNorm1d(128)
        self.act3 = nn.GELU()
        self.drop3 = nn.Dropout(dropout_rate)
                
        self.fc4 = nn.Linear(128, 1)
                
    # Прямое прохождение сети
    def forward(self, x):
                
        x = self.fc1(x)
        x = self.bn1(x)
        x = self.act1(x)
        x = self.drop1(x)
                
        x = self.fc2(x)
        x = self.bn2(x)
        x = self.act2(x)
        x = self.drop2(x)
                
        x = self.fc3(x)
        x = self.bn3(x)
        x = self.act3(x)
        x = self.drop3(x)
                
        x = self.fc4(x)
                
        return x

Подбор гиперпараметров для нейронной сети¶

In [60]:
# Подбираемые гиперпараметры
lst_dropout = [0, 0.05, 0.1, 0.2]
lst_batch = [8, 16, 32, 64]

# Переменные для сохранения лучших результатов
best_model = None
best_pred = None
best_rmse_overall = float('inf')
best_hyperparams = {'dropout': None, 'batch_size': None}

# Цикл подбора гиперпараметров
for dropout in lst_dropout:
    for batch in lst_batch:
        
        # Создаём DataLoader для батчевого обучения
        dataset = TensorDataset(X_train_scale, y_train_tensor)
        dataloader = DataLoader(dataset, batch_size=batch, shuffle=True)
        
        # Инициализация сети
        net_2 = Net_2(dropout_rate=dropout)
        optimizer_2 = torch.optim.Adam(net_2.parameters(), lr=0.001, weight_decay=1e-6)
        loss_2 = nn.MSELoss()
        num_epochs = 5000
        
        # Инициализация весов
        net_2.apply(init_weights)
        
        # Параметры обучения
        patience = 100
        min_delta = 1e-6
        best_rmse = float('inf')
        patience_count = 0
        
        # Обучение модели
        early_stopped = True
        for epoch in range(num_epochs):
            
            net_2.train()
            
            for X_batch, y_batch in dataloader:
                
                optimizer_2.zero_grad()
                preds = net_2(X_batch).flatten()
                
                loss_value = loss_2(preds, y_batch)
                loss_value.backward()
            
                optimizer_2.step()
            
            # Оценка модели
            net_2.eval()
            
            with torch.no_grad():
                
                pred_2 = net_2(X_test_scale).flatten()
                pred_2 = np.exp(pred_2.numpy())
                rmse_2 = np.sqrt(mean_squared_error(y_test_numpy, pred_2))
                
            # Ранняя остановка
            if rmse_2 < best_rmse - min_delta:
                best_rmse = rmse_2
                patience_count = 0
                
            else:
                patience_count += 1
                if patience_count >= patience:
                    print(f'\nHyperparameters => DropOut = {dropout} | Batch_size = {batch}')
                    print(f'Early stopping at epoch {epoch}, final RMSE: {rmse_2:.6f}')
                    early_stopped = False
                    break
        
        if early_stopped:
            # Результаты обучения
            print(f'\nHyperparameters => DropOut = {dropout} | Batch_size = {batch}')
            print(f'Epoch {epoch}, RMSE: {rmse_2:.6f}')
        
        # Проверка, является ли текущая модель лучшей
        if rmse_2 < best_rmse_overall:
            best_rmse_overall = rmse_2
            best_model = net_2.state_dict()
            best_hyperparams = {'dropout': dropout, 'batch_size': batch}
            best_pred = pred_2

print(f'\nBest hyperparameters: {best_hyperparams}')
print(f'Best RMSE: {best_rmse_overall:.6f}')
Hyperparameters => DropOut = 0 | Batch_size = 8
Early stopping at epoch 128, final RMSE: 5678.773283

Hyperparameters => DropOut = 0 | Batch_size = 16
Early stopping at epoch 169, final RMSE: 6314.550499

Hyperparameters => DropOut = 0 | Batch_size = 32
Early stopping at epoch 294, final RMSE: 3714.305722

Hyperparameters => DropOut = 0 | Batch_size = 64
Early stopping at epoch 204, final RMSE: 3358.101696

Hyperparameters => DropOut = 0.05 | Batch_size = 8
Early stopping at epoch 191, final RMSE: 6052.819508

Hyperparameters => DropOut = 0.05 | Batch_size = 16
Early stopping at epoch 225, final RMSE: 4567.504789

Hyperparameters => DropOut = 0.05 | Batch_size = 32
Early stopping at epoch 241, final RMSE: 4001.724128

Hyperparameters => DropOut = 0.05 | Batch_size = 64
Early stopping at epoch 256, final RMSE: 2911.542375

Hyperparameters => DropOut = 0.1 | Batch_size = 8
Early stopping at epoch 280, final RMSE: 4617.211063

Hyperparameters => DropOut = 0.1 | Batch_size = 16
Early stopping at epoch 490, final RMSE: 4878.387029

Hyperparameters => DropOut = 0.1 | Batch_size = 32
Early stopping at epoch 262, final RMSE: 3280.235662

Hyperparameters => DropOut = 0.1 | Batch_size = 64
Early stopping at epoch 281, final RMSE: 4315.550950

Hyperparameters => DropOut = 0.2 | Batch_size = 8
Early stopping at epoch 284, final RMSE: 7201.126856

Hyperparameters => DropOut = 0.2 | Batch_size = 16
Early stopping at epoch 290, final RMSE: 4384.024635

Hyperparameters => DropOut = 0.2 | Batch_size = 32
Early stopping at epoch 416, final RMSE: 4538.823195

Hyperparameters => DropOut = 0.2 | Batch_size = 64
Early stopping at epoch 507, final RMSE: 4469.405106

Best hyperparameters: {'dropout': 0.05, 'batch_size': 64}
Best RMSE: 2911.542375

Наилучшие гиперпараметры для модели оказались DropOut = 0.05, Batch_size = 64, добавлением регуляризации и обучения через батчи нам удалось улучшить метрику качества модели до RMSE = 2911.542.

Анализ результатов:¶

Проанализируем прогноз лучшей модели с помощью графика.

In [61]:
analyzis_test(y_test, best_pred)
No description has been provided for this image

Рассмотрим строки с звездами, где прогноз сильно отличается от факта.

In [62]:
# Список индексов интересующих нас звезд
idx_star_false = [102, 105, 118, 141, 154, 156, 159, 162, 163, 165, 173, 177, 179, 205, 207, 221]

# Фильтруем таблицу, оставляя только интересующия нас звезды
false_predict = df.loc[df.index.isin(idx_star_false)]

false_predict
Out[62]:
temperature luminosity radius abs_magnitude star_type star_color
102 23000 127000.00000 36.0000 -5.76 4 blue
105 14245 231000.00000 42.0000 -6.12 4 blue
118 6850 229000.00000 1467.0000 -10.07 5 red
141 21020 0.00150 0.0112 11.52 2 blue
154 25070 14500.00000 5.9200 -3.98 3 blue_white
156 26140 14520.00000 5.4900 -3.80 3 blue_white
159 37800 202900.00000 6.8600 -4.56 3 blue
162 12675 452000.00000 83.0000 -5.62 4 blue
163 5752 245000.00000 97.0000 -6.63 4 blue
165 7282 131000.00000 24.0000 -7.22 4 blue
173 26000 316000.00000 1679.0000 -9.10 5 blue
177 11000 170000.00000 1779.0000 -9.90 5 blue_white
179 24490 248490.00000 1134.5000 -8.24 5 blue_white
205 19920 0.00156 0.0142 11.34 2 blue
207 23092 0.00132 0.0104 10.18 2 blue
221 12749 332520.00000 76.0000 -7.02 4 blue

Классификация звезд

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

Аугментация данных¶

Попробуем добавить больше данных, с помощью их синтезирования. Получим интересующий нас сегмент из обучающей выборки, а именно звезды 4 и 5 типа и голубых, бело-голубых цветов.

In [63]:
aug_df = pd.concat([X_train.copy(), y_train.copy()], axis=1)
aug_df.shape
Out[63]:
(180, 6)
In [64]:
aug_df = aug_df[(aug_df['star_type'] >= 4) & aug_df['star_color'].isin(['blue', 'blue_white'])]
aug_df.shape
Out[64]:
(32, 6)
In [65]:
# Получаем абсолютные значения радиуса звезд
aug_df['abs_radius'] = aug_df['radius'] * 6.9551 * 10**8
In [66]:
# Считаем статистики для каждой подгруппы
aug_stat = aug_df.groupby(['star_type', 'star_color'], as_index=False)\
                 .agg(temperature_mean=('temperature', 'mean'),
                      temperature_std=('temperature', 'std'), 
                      radius_mean=('abs_radius', 'mean'), 
                      radius_std=('abs_radius', 'std'))
aug_stat
Out[66]:
star_type star_color temperature_mean temperature_std radius_mean radius_std
0 4 blue 20565.130435 9120.868262 3.559197e+10 1.899375e+10
1 5 blue 32730.666667 7949.944394 1.006461e+12 2.422479e+11
2 5 blue_white 24596.000000 2943.527985 8.985989e+11 1.288553e+11
In [67]:
# Генерация аугментированных данных
augmented_rows = []

n_samples_per_group = 50
rng = np.random.default_rng(seed=RANDOM_STATE)

for _, row in aug_stat.iterrows():
    # Генерируем температуры и радиусы
    temps = rng.normal(loc=row["temperature_mean"], scale=row["temperature_std"], size=n_samples_per_group)
    radii = rng.normal(loc=row["radius_mean"], scale=row["radius_std"], size=n_samples_per_group)
    
    # Проверка значений
    temps = np.where(temps <= 0, np.nan, temps)
    radii = np.where(radii <= 0, np.nan, radii)
    
    # Добавляем строки
    for t, r in zip(temps, radii):
        augmented_rows.append({
            "star_type": row["star_type"],
            "star_color": row["star_color"],
            "temperature": t,
            "abs_radius": r
        })
        
# Собираем в датафрейм
augmented_df = pd.DataFrame(augmented_rows)
# Удаляем некорректные значения
augmented_df = augmented_df.dropna()
print(f'Размер аугментированных данных: {augmented_df.shape}')
augmented_df.head()
Размер аугментированных данных: (149, 4)
Out[67]:
star_type star_color temperature abs_radius
0 4 blue 18660.460336 6.078627e+10
1 4 blue 12934.574875 1.685283e+10
2 4 blue 27720.737835 6.486470e+10
3 4 blue 20068.752831 2.036209e+10
4 4 blue 22782.496588 2.529064e+10
In [68]:
# Сравним статистики данных
print('Статистика оригинальных данных')
display(aug_stat)

aug_stat_new = augmented_df.groupby(['star_type', 'star_color'], as_index=False)\
                           .agg(temperature_mean=('temperature', 'mean'),
                                temperature_std=('temperature', 'std'), 
                                radius_mean=('abs_radius', 'mean'), 
                                radius_std=('abs_radius', 'std'))
print()
print('Статистика аугментированных данных')
display(aug_stat_new)
Статистика оригинальных данных
star_type star_color temperature_mean temperature_std radius_mean radius_std
0 4 blue 20565.130435 9120.868262 3.559197e+10 1.899375e+10
1 5 blue 32730.666667 7949.944394 1.006461e+12 2.422479e+11
2 5 blue_white 24596.000000 2943.527985 8.985989e+11 1.288553e+11
Статистика аугментированных данных
star_type star_color temperature_mean temperature_std radius_mean radius_std
0 4 blue 21126.820763 9241.931930 3.324662e+10 1.988440e+10
1 5 blue 31016.029363 9645.397356 1.009009e+12 2.859856e+11
2 5 blue_white 24825.475249 3569.051836 8.919971e+11 1.401590e+11

Статистики отличаются несущественно, учитывая такие малые группы, вычислим остальные признаки с помощью ранее озвученных формул.

Абсолютная визуальная звёздная величина:

$$ M_V = -2.5 \cdot \log_{10} \left( \frac{L_V}{L_{V,\odot}} \right) + M_{V,\odot} $$

где:

  • ($M_V$) — абсолютная визуальная величина звезды,
  • ($L_V$) — светимость звезды в визуальном диапазоне,
  • ($L_{V,\odot}$) — светимость Солнца в том же диапазоне (≈ 3.828 × 10²⁶ Вт),
  • ($M_{V,\odot}$) — абсолютная визуальная величина Солнца (≈ 4.83).

Светимость через радиус и температуру (закон Стефана–Больцмана):

$$ L = 4 \pi R^2 \sigma T^4 $$

где:

  • ($L$) — полная светимость звезды,
  • ($R$) — радиус звезды,
  • ($T$) — эффективная температура звезды (в Кельвинах),
  • ($\sigma$) — постоянная Стефана–Больцмана (≈ 5.67 × 10⁻⁸ Вт·м⁻²·К⁻⁴).
In [69]:
# Обозначим переменные для расчета признаков
L_0 = 3.828 * 10**26
R_0 = 6.9551 * 10**8
Mv_0 = 4.83
sigma = 5.67 * 10**(-8)
In [70]:
# Расчет необходимых признаков
augmented_df['radius'] = augmented_df['abs_radius'] / R_0
augmented_df['luminosity'] = (
    4 * np.pi * augmented_df['abs_radius']**2 * sigma * augmented_df['temperature']**4 / L_0
)
augmented_df['abs_magnitude'] = -2.5 * np.log10(augmented_df['luminosity']) + Mv_0

Сравним статистики по каждому признаку.

In [71]:
display(aug_df.describe())
display(augmented_df.describe())
luminosity radius abs_magnitude star_type temperature abs_radius
count 32.000000 32.000000 32.000000 32.000000 32.000000 3.200000e+01
mean 393076.187500 429.234375 -7.098344 4.281250 23224.062500 2.985368e+11
std 231730.378052 633.174401 1.441180 0.456803 9628.636647 4.403791e+11
min 112000.000000 12.000000 -10.840000 4.000000 8927.000000 8.346120e+09
25% 224335.000000 35.750000 -7.610000 4.000000 16063.250000 2.486448e+10
50% 318921.500000 70.000000 -6.420000 4.000000 23559.000000 4.868570e+10
75% 551547.500000 1066.250000 -5.986250 5.000000 31251.500000 7.415875e+11
max 849420.000000 1948.500000 -5.690000 5.000000 40000.000000 1.355201e+12
star_type temperature abs_radius radius luminosity abs_magnitude
count 149.000000 149.000000 1.490000e+02 149.000000 1.490000e+02 149.000000
mean 4.671141 25686.506362 6.488548e+11 932.919491 1.225500e+09 -14.313898
std 0.471383 8913.496948 4.722023e+11 678.929622 2.807957e+09 5.101670
min 4.000000 317.985995 3.069366e+09 4.413116 1.572303e-02 -21.089505
25% 4.000000 20312.519810 5.054516e+10 72.673519 1.087675e+06 -17.811671
50% 5.000000 25270.812916 8.179056e+11 1175.979680 3.857373e+08 -16.635729
75% 5.000000 30757.170673 1.003626e+12 1443.006636 1.139380e+09 -10.261247
max 5.000000 53310.454445 1.466221e+12 2108.123433 2.332393e+10 9.338659
In [72]:
# Убираем строки с выбивающемся из общей картины значениями
augmented_df_clear = augmented_df[(augmented_df['temperature'] >= 8927) &
                                  (augmented_df['radius'] >= 12) &
                                  (augmented_df['luminosity'] >= 112000)]
In [73]:
display(aug_df.describe())
display(augmented_df_clear.describe())
luminosity radius abs_magnitude star_type temperature abs_radius
count 32.000000 32.000000 32.000000 32.000000 32.000000 3.200000e+01
mean 393076.187500 429.234375 -7.098344 4.281250 23224.062500 2.985368e+11
std 231730.378052 633.174401 1.441180 0.456803 9628.636647 4.403791e+11
min 112000.000000 12.000000 -10.840000 4.000000 8927.000000 8.346120e+09
25% 224335.000000 35.750000 -7.610000 4.000000 16063.250000 2.486448e+10
50% 318921.500000 70.000000 -6.420000 4.000000 23559.000000 4.868570e+10
75% 551547.500000 1066.250000 -5.986250 5.000000 31251.500000 7.415875e+11
max 849420.000000 1948.500000 -5.690000 5.000000 40000.000000 1.355201e+12
star_type temperature abs_radius radius luminosity abs_magnitude
count 131.000000 131.000000 1.310000e+02 131.000000 1.310000e+02 131.000000
mean 4.763359 27156.732796 7.355002e+11 1057.497656 1.393886e+09 -15.683335
std 0.426652 7787.533754 4.372621e+11 628.692695 2.956320e+09 3.445808
min 4.000000 10357.262339 1.218603e+10 17.521005 1.251836e+05 -21.089505
25% 5.000000 22232.004958 4.008421e+11 576.328333 5.020834e+07 -18.030659
50% 5.000000 26040.478460 8.583379e+11 1234.112925 4.660033e+08 -16.840972
75% 5.000000 31307.952796 1.028665e+12 1479.007967 1.394051e+09 -14.420423
max 5.000000 53310.454445 1.466221e+12 2108.123433 2.332393e+10 -7.913868

Мы получили 131 объект, который схож по статистикам с обучающей выборкой, добавим эти данные в тренировочную выборку и обучим модель заново на подобранных гиперпараметрах.

In [74]:
# Добавление аугментированных данных к обучающей выборке
augmented_df_clear = augmented_df_clear.drop('abs_radius', axis=1)

feature_cols = X_train.columns.to_list()

X_aug = augmented_df_clear[feature_cols]
y_aug = augmented_df_clear['temperature']

X_train_new = pd.concat([X_train, X_aug], axis=0, ignore_index=True)
y_train_new = pd.concat([y_train, y_aug], axis=0, ignore_index=True)

# Проверяем размер новой выборки
print(f'Размер новой обучающей выборки: {X_train_new.shape, y_train_new.shape}')
Размер новой обучающей выборки: ((311, 5), (311,))
In [75]:
# Подготовка данных к обучению модели
X_train_scale_new = processor.fit_transform(X_train_new)
y_train_tensor_new = torch.FloatTensor(np.log(y_train_new).values)

# Тестовые данные
X_test_scale_new = processor.transform(X_test)
y_test_numpy = y_test.values

Обучение модели и подбор гиперпараметров на дополненной тренировочной выборке:¶

In [76]:
# Подбираемые гиперпараметры
lst_dropout = [0, 0.05, 0.1, 0.2]
lst_batch = [8, 16, 32, 64]

# Переменные для сохранения лучших результатов
best_model_aug = None
best_pred_aug = None
best_rmse_overall_aug = float('inf')
best_hyperparams_aug = {'dropout': None, 'batch_size': None}

# Цикл подбора гиперпараметров
for dropout in lst_dropout:
    for batch in lst_batch:
        
        # Создаём DataLoader для батчевого обучения
        dataset = TensorDataset(X_train_scale_new, y_train_tensor_new)
        dataloader = DataLoader(dataset, batch_size=batch, shuffle=True)
        
        # Инициализация сети
        net_3 = Net_2(dropout_rate=dropout)
        optimizer_3 = torch.optim.Adam(net_3.parameters(), lr=0.001, weight_decay=1e-6)
        loss_3 = nn.MSELoss()
        num_epochs = 5000
        
        # Инициализация весов
        net_3.apply(init_weights)
        
        # Параметры обучения
        patience = 100
        min_delta = 1e-6
        best_rmse = float('inf')
        patience_count = 0
        
        # Обучение модели
        early_stopped = True
        for epoch in range(num_epochs):
            
            net_3.train()
            
            for X_batch, y_batch in dataloader:
                
                optimizer_3.zero_grad()
                preds = net_3(X_batch).flatten()
                
                loss_value = loss_3(preds, y_batch)
                loss_value.backward()
            
                optimizer_3.step()
            
            # Оценка модели
            net_3.eval()
            
            with torch.no_grad():
                
                pred_3 = net_3(X_test_scale_new).flatten()
                pred_3 = np.exp(pred_3.numpy())
                rmse_3 = np.sqrt(mean_squared_error(y_test_numpy, pred_3))
                
            # Ранняя остановка
            if rmse_3 < best_rmse - min_delta:
                best_rmse = rmse_3
                patience_count = 0
                
            else:
                patience_count += 1
                if patience_count >= patience:
                    print(f'\nHyperparameters => DropOut = {dropout} | Batch_size = {batch}')
                    print(f'Early stopping at epoch {epoch}, final RMSE: {rmse_3:.6f}')
                    early_stopped = False
                    break
        
        if early_stopped:
            # Результаты обучения
            print(f'\nHyperparameters => DropOut = {dropout} | Batch_size = {batch}')
            print(f'Epoch {epoch}, RMSE: {rmse_3:.6f}')
        
        # Проверка, является ли текущая модель лучшей
        if rmse_3 < best_rmse_overall_aug:
            best_rmse_overall_aug = rmse_3
            best_model_aug = net_3.state_dict()
            best_hyperparams_aug = {'dropout': dropout, 'batch_size': batch}
            best_pred_aug = pred_3

print(f'\nBest hyperparameters: {best_hyperparams_aug}')
print(f'Best RMSE: {best_rmse_overall_aug:.6f}')
Hyperparameters => DropOut = 0 | Batch_size = 8
Early stopping at epoch 505, final RMSE: 4034.840517

Hyperparameters => DropOut = 0 | Batch_size = 16
Early stopping at epoch 217, final RMSE: 4651.866292

Hyperparameters => DropOut = 0 | Batch_size = 32
Early stopping at epoch 392, final RMSE: 3405.812825

Hyperparameters => DropOut = 0 | Batch_size = 64
Early stopping at epoch 275, final RMSE: 3235.010201

Hyperparameters => DropOut = 0.05 | Batch_size = 8
Early stopping at epoch 290, final RMSE: 4269.167132

Hyperparameters => DropOut = 0.05 | Batch_size = 16
Early stopping at epoch 250, final RMSE: 5493.369458

Hyperparameters => DropOut = 0.05 | Batch_size = 32
Early stopping at epoch 455, final RMSE: 4572.635564

Hyperparameters => DropOut = 0.05 | Batch_size = 64
Early stopping at epoch 447, final RMSE: 3920.590644

Hyperparameters => DropOut = 0.1 | Batch_size = 8
Early stopping at epoch 494, final RMSE: 3821.246132

Hyperparameters => DropOut = 0.1 | Batch_size = 16
Early stopping at epoch 287, final RMSE: 3930.638243

Hyperparameters => DropOut = 0.1 | Batch_size = 32
Early stopping at epoch 313, final RMSE: 3287.566273

Hyperparameters => DropOut = 0.1 | Batch_size = 64
Early stopping at epoch 345, final RMSE: 4729.740162

Hyperparameters => DropOut = 0.2 | Batch_size = 8
Early stopping at epoch 517, final RMSE: 4272.630571

Hyperparameters => DropOut = 0.2 | Batch_size = 16
Early stopping at epoch 299, final RMSE: 5222.080620

Hyperparameters => DropOut = 0.2 | Batch_size = 32
Early stopping at epoch 346, final RMSE: 3310.133985

Hyperparameters => DropOut = 0.2 | Batch_size = 64
Early stopping at epoch 297, final RMSE: 4213.942809

Best hyperparameters: {'dropout': 0, 'batch_size': 64}
Best RMSE: 3235.010201
In [77]:
analyzis_test(y_test, best_pred_aug)
No description has been provided for this image
In [78]:
# Список индексов интересующих нас звезд
idx_star_false = [102, 118, 141, 143, 154, 156, 159, 162, 163, 165, 173, 177, 178, 179, 205, 207, 209]

# Фильтруем таблицу, оставляя только интересующия нас звезды
false_predict_aug = df.loc[df.index.isin(idx_star_false)]

false_predict_aug
Out[78]:
temperature luminosity radius abs_magnitude star_type star_color
102 23000 127000.00000 36.00000 -5.76 4 blue
118 6850 229000.00000 1467.00000 -10.07 5 red
141 21020 0.00150 0.01120 11.52 2 blue
143 14520 0.00082 0.00972 11.92 2 blue_white
154 25070 14500.00000 5.92000 -3.98 3 blue_white
156 26140 14520.00000 5.49000 -3.80 3 blue_white
159 37800 202900.00000 6.86000 -4.56 3 blue
162 12675 452000.00000 83.00000 -5.62 4 blue
163 5752 245000.00000 97.00000 -6.63 4 blue
165 7282 131000.00000 24.00000 -7.22 4 blue
173 26000 316000.00000 1679.00000 -9.10 5 blue
177 11000 170000.00000 1779.00000 -9.90 5 blue_white
178 12100 120000.00000 708.90000 -7.84 5 blue_white
179 24490 248490.00000 1134.50000 -8.24 5 blue_white
205 19920 0.00156 0.01420 11.34 2 blue
207 23092 0.00132 0.01040 10.18 2 blue
209 19360 0.00125 0.00998 11.62 2 blue

После добавления аугментированных данных мы получили метрику модели RMSE = 3235.01, к сожалению данных подход к аугментированию данных оказался неудачным.

Подготовка итогой сводной таблицы¶

In [79]:
pivot_table = {'Model': ['Baseline', 
                         'Baseline Neural Network', 
                         'Upgrade Neural Network', 
                         'Neural Network Augmented'], 
              'Description': ['Прогнозирование температуры с помощью закона Стефана-Больцмана', 
                              'Базовая нейросеть', 
                              'Улучшенная базовая нейросеть с подобранными гиперпараметрами', 
                              'Улучшенная базовая нейросеть обученная с добавлением синтетических данных'],
              'Hyperparameters': [' — ', 
                                  ' — ', 
                                  best_hyperparams, 
                                  best_hyperparams_aug], 
              'RMSE': [rmse_bf, baseline_rmse, best_rmse_overall, best_rmse_overall_aug]}
pivot_table = pd.DataFrame(pivot_table).sort_values(by='RMSE').reset_index(drop=True)
pivot_table['RMSE'] = pivot_table['RMSE'].round(3)
pivot_table
Out[79]:
Model Description Hyperparameters RMSE
0 Upgrade Neural Network Улучшенная базовая нейросеть с подобранными гиперпараметрами {'dropout': 0.05, 'batch_size': 64} 2911.542
1 Neural Network Augmented Улучшенная базовая нейросеть обученная с добавлением синтетических данных {'dropout': 0, 'batch_size': 64} 3235.010
2 Baseline Neural Network Базовая нейросеть — 3360.929
3 Baseline Прогнозирование температуры с помощью закона Стефана-Больцмана — 7646.102

Вывод:¶

На данном этапе работы мы попробовали следующие способы улучшения базовой нейросети:

  • Обучение с помощью батчей;
  • Добавление регуляризации;
  • Подбор гиперпараметров с помощью цикла;
  • Добавление аугментированных данных в тренировочную выборку.

После добавления обучения c помощью батчей и регуляризации в модель, нам удалось улучшить метрику базовой нейросети на ~13%.

Сравнение прогноза улучшенной нейросети с фактическими значениями на тестовой выборке:

In [80]:
analyzis_test(y_test, best_pred)
No description has been provided for this image

Звезды, где нейросеть ошиблась сильнее всего:

In [81]:
false_predict
Out[81]:
temperature luminosity radius abs_magnitude star_type star_color
102 23000 127000.00000 36.0000 -5.76 4 blue
105 14245 231000.00000 42.0000 -6.12 4 blue
118 6850 229000.00000 1467.0000 -10.07 5 red
141 21020 0.00150 0.0112 11.52 2 blue
154 25070 14500.00000 5.9200 -3.98 3 blue_white
156 26140 14520.00000 5.4900 -3.80 3 blue_white
159 37800 202900.00000 6.8600 -4.56 3 blue
162 12675 452000.00000 83.0000 -5.62 4 blue
163 5752 245000.00000 97.0000 -6.63 4 blue
165 7282 131000.00000 24.0000 -7.22 4 blue
173 26000 316000.00000 1679.0000 -9.10 5 blue
177 11000 170000.00000 1779.0000 -9.90 5 blue_white
179 24490 248490.00000 1134.5000 -8.24 5 blue_white
205 19920 0.00156 0.0142 11.34 2 blue
207 23092 0.00132 0.0104 10.18 2 blue
221 12749 332520.00000 76.0000 -7.02 4 blue

После анализа результатов мы обнаружили, что нейросеть недостаточно хорошо справляется с звездами 4 и 5 типа и голубого / бело-голубого цвета. Поэтому было принято решение добавить аугментированных данных, на основе имеющейся тренировочной выборки. К сожалению данный метод не дал должного результата и метрика оказалась хуже, чем у модели обученной на оригинальных данных.

Итоговые результаты в сводной таблице:

In [82]:
pivot_table
Out[82]:
Model Description Hyperparameters RMSE
0 Upgrade Neural Network Улучшенная базовая нейросеть с подобранными гиперпараметрами {'dropout': 0.05, 'batch_size': 64} 2911.542
1 Neural Network Augmented Улучшенная базовая нейросеть обученная с добавлением синтетических данных {'dropout': 0, 'batch_size': 64} 3235.010
2 Baseline Neural Network Базовая нейросеть — 3360.929
3 Baseline Прогнозирование температуры с помощью закона Стефана-Больцмана — 7646.102

По итогам работы лучшей моделью стала Upgrade Neural Network.

Выполнены все поставленные задачи работы, найден метод, который лучше прогнозирует температуру звезды с помощью нейросети, а так же достигнут порог требуемого качества прогноза, значение RMSE ниже 4500, она составляет 2911.542.

Общие выводы:¶

Описание проекта:

Обычно для расчёта температуры звезды учёные пользуются следующими методами:

  • Закон смещения Вина.
  • Закон Стефана-Больцмана.
  • Спектральный анализ.

Каждый из них имеет плюсы и минусы. Обсерватория хочет внедрить технологии машинного обучения для предсказания температуры звёзд, надеясь, что этот метод будет наиболее точным и удобным. В базе обсерватории есть характеристики уже изученных 240 звёзд.

Цель проекта: Найти решение задачи регрессии по определению температуры на поверхности обнаруженных звёзд с помощью нейросети и точностью прогноза RMSE <= 4500.

Описание данных:

6_class.csv — описание уже изученных звезд.

Описание признаков:

  • Luminosity(L/Lo) — светимость звезды относительно Солнца;
  • Radius(R/Ro) — радиус звезды относительно радиуса Солнца;
  • Absolute magnitude(Mv) — абсолютная звёздная величина, физическая величина, характеризующая блеск звезды;
  • Star color — (white, red, blue, yellow, yellow-orange и др.) — цвет звезды, который определяют на основе спектрального анализа;
  • Star type — тип звезды (описание типов в таблице ниже);
Тип звезды Номер, соответствующий типу
Коричневый карлик 0
Красный карлик 1
Белый карлик 2
Звёзды главной последовательности 3
Сверхгигант 4
Гипергигант 5
  • Temperature (K)— (Целевой признак) — абсолютная температура на поверхности звезды в Кельвинах.

Ход исследования:

  • Подготовка данных: загрузка и изучение общей информации из представленных датасетов.

  • Предобработка данных: стандартизация данных и обработка явных и неявных дубликатов.

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

  • Корреляционный анализ: изучение взимосвязей между входными признаками и целевыми, а также и между ними.

  • Подготовка данных к обучению модели: деление данных на тестовую и тренировочную выборки, feature engineering, отбор признаков для обучения с помощью mutual_info_regression, построение пайплайна обработки данных.

  • Построение базовой нейронной сети: получение бызовой метрики по тестовой выборки с помощью закона Стефана-Больцмана, построение базовой нейронной сети, где определяется архитектура модели и получение метрики качества ее прогноза на тестовой выборке и анализ результатов.

  • Улучшение нейронной сети: подбор гиперпараметров для ранее полученной нейросети, добавление регуляризации и обучения с помощью батчей, получение метрики качества ее прогноза на тестовой выборке и анализ результатов, тестируется добавление аугментированных данных для улучшения точности прогноза нейросети.

  • Общий вывод: резюмирование полученных результатов, формулировка ключевых выводов.

Результаты работы:

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

Метрики качества RMSE:

  • Закон Стефана-Больцмана — 7646.102
  • Базовая нейросеть — ~3360.929

Проанализировав прогнозы модели на тестовой выборке, сделали следующие выводы, что у модели сложности с голубыми звездами 4 и 5 типа. Для повышения точности прогноза модели были использованы следующие инструменты:

  • Обучение с помощью батчей;
  • Добавление регуляризации;
  • Подбор гиперпараметров с помощью цикла;
  • Добавление аугментированных данных в тренировочную выборку.

После добавления обучения c помощью батчей и регуляризации в модель, нам удалось улучшить метрику базовой нейросети на ~13%.

Сравнение прогноза улучшенной нейросети с фактическими значениями на тестовой выборке:

In [83]:
analyzis_test(y_test, best_pred)
No description has been provided for this image

Звезды, где нейросеть ошиблась сильнее всего:

In [84]:
false_predict
Out[84]:
temperature luminosity radius abs_magnitude star_type star_color
102 23000 127000.00000 36.0000 -5.76 4 blue
105 14245 231000.00000 42.0000 -6.12 4 blue
118 6850 229000.00000 1467.0000 -10.07 5 red
141 21020 0.00150 0.0112 11.52 2 blue
154 25070 14500.00000 5.9200 -3.98 3 blue_white
156 26140 14520.00000 5.4900 -3.80 3 blue_white
159 37800 202900.00000 6.8600 -4.56 3 blue
162 12675 452000.00000 83.0000 -5.62 4 blue
163 5752 245000.00000 97.0000 -6.63 4 blue
165 7282 131000.00000 24.0000 -7.22 4 blue
173 26000 316000.00000 1679.0000 -9.10 5 blue
177 11000 170000.00000 1779.0000 -9.90 5 blue_white
179 24490 248490.00000 1134.5000 -8.24 5 blue_white
205 19920 0.00156 0.0142 11.34 2 blue
207 23092 0.00132 0.0104 10.18 2 blue
221 12749 332520.00000 76.0000 -7.02 4 blue

После анализа результатов мы обнаружили, что нейросеть недостаточно хорошо справляется с звездами 4 и 5 типа и голубого / бело-голубого цвета. Поэтому было принято решение добавить аугментированных данных, на основе имеющейся тренировочной выборки. К сожалению данный метод не дал должного результата и метрика оказалась хуже, чем у модели обученной на оригинальных данных.

Итоговые результаты в сводной таблице:

In [85]:
pivot_table
Out[85]:
Model Description Hyperparameters RMSE
0 Upgrade Neural Network Улучшенная базовая нейросеть с подобранными гиперпараметрами {'dropout': 0.05, 'batch_size': 64} 2911.542
1 Neural Network Augmented Улучшенная базовая нейросеть обученная с добавлением синтетических данных {'dropout': 0, 'batch_size': 64} 3235.010
2 Baseline Neural Network Базовая нейросеть — 3360.929
3 Baseline Прогнозирование температуры с помощью закона Стефана-Больцмана — 7646.102

По итогам работы лучшей моделью стала Upgrade Neural Network.

Выполнены все поставленные задачи работы, найден метод, который лучше прогнозирует температуру звезды с помощью нейросети, а так же достигнут порог требуемого качества прогноза, значение RMSE ниже 4500, она составляет 2911.542.