惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
博客园 - 三生石上(FineUI控件)
S
Securelist
U
Unit 42
The Cloudflare Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Simon Willison's Weblog
Simon Willison's Weblog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
B
Blog
T
Tenable Blog
The Hacker News
The Hacker News
The Register - Security
The Register - Security
IT之家
IT之家
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
P
Privacy & Cybersecurity Law Blog
博客园_首页
T
Tailwind CSS Blog
人人都是产品经理
人人都是产品经理
C
Cybersecurity and Infrastructure Security Agency CISA
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
T
Tor Project blog
C
CERT Recently Published Vulnerability Notes
Apple Machine Learning Research
Apple Machine Learning Research
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
V
Vulnerabilities – Threatpost
A
Arctic Wolf
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
V
V2EX
aimingoo的专栏
aimingoo的专栏
大猫的无限游戏
大猫的无限游戏
Scott Helme
Scott Helme
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
V
Visual Studio Blog
月光博客
月光博客
爱范儿
爱范儿
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
H
Heimdal Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Разбираемся в ML без воды: от базы до Attention. Часть 3
ysrgsyn · 2026-05-22 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение11 мин

Охват и читатели1.5K

Туториал

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

Логическим продолжением будет изучение (не побоюсь этого слова) сердца машинного обучения: градиентного спуска. Итак

Градиентный спуск

Так как лучше уже не придумаешь, начнем с классического рассказа о пьяном альпинисте:

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

Что он будет делать, чтобы спуститься вниз? Алгоритм максимально прост:

  1. Он аккуратно выставляет ногу вперед, влево, вправо и прощупывает подошвой ботинка рельеф под собой, тем самым определяет в какую сторону идет уклон горы;

  2. Когда понимает, где склон уходит вниз делает туда ровно один шаг.

  3. Повторяет процесс заново

Вопрос։ При каких условиях альпинист не дойдет до ущелья (по крайней мере живым)?

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

Во-вторых, шаги альпиниста не должны быть километровыми. Какой смысл в том, что он каким-то способом сделает шаг и попадет на вершину соседней горы?

В-третьих, противоположный случай: если он сделает микроскопические шаги, пройдут года, наступит старость и т.д.

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

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

Определим градиент. Градиентом функции f(x_1 \dots x_n) называется векторная функция вида

 \nabla f = \text{grad}\,f = \left( \frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, \dots, \frac{\partial f}{\partial x_n} \right)

Естественно, градиент (в том виде, в котором он нам интересен) существует не всегда: функция должна быть дифференцируемой. Это условие (почти) то же самое, что и гладкость горы для альпиниста.

Почему почти

В математике существует понятие гладкой функции. В данном контексте будем понимать под этим функции класса C^1: множество функций, которые являются непрерывно дифференцируемыми.

Для функции многих переменных (что типично для ML) принадлежность к классу C^1 на некотором множестве означает выполнение двух условий:

  • Существование: В каждой точке существуют все частные производные первого порядка.

  • Непрерывность: Все частные производные первого порядка сами по себе являются непрерывными функциями.

Возможно я путаюсь, и гладкими называются функции классаC^{\infty}. Но на мою память у них название "бесконечно гладкие". Вроде как устойчивого определения не существует, и всё зависит от литературы. Знающие, если я ошибся, поправьте, пожалуйста.

Не будем выводить свойства градиента, но отмечу, зачем нам он нужен։ Оказывается, градиент показывает направление наибольшего роста функции. И, стало быть, тот же градиент со знаком минус (a.k.a. антиградиент) показывает направление, где функция убывает быстрее всего. Сохраняя аналогию, это тот самый ботинок альпиниста, которым он нащупывает гору.

Вот и математическое понимание градиентного спуска: Ищем антиградиент функции потерь, идём по нему, пока не дойдем до минимума.

В формуле вся эта логика «прощупывания и шага» превращается в одну простую строчку, по которой компьютер обновляет параметры (веса) модели на каждой итерации:

w^{new} = w^{old} - \eta \cdot \nabla L(w^{old})

Здесь w^{old} — текущие (пока еще неточные) веса нашей модели. Это точка на склоне, где альпинист стоит прямо сейчас.
\nabla L(w^{old}) — вектор градиента нашей функции потерь. Тот самый компас, показывающий на вершину горы ошибок.
Знак минус разворачивает нас спиной к вершине и лицом к ущелью (превращает градиент в антиградиент).
\etaкоэффициент скорости обучения, он же learning rate (lr). Тот самый размер шага альпиниста. Она, кстати говоря является гиперпараметром (то есть параметром, который мы выбираем руками еще до старта, а не модель подбирает сама в процессе обучения). Также она может быть как фиксированной, так и динамически изменяемой (например, уменьшаться по мере приближения к минимуму, чтобы наш альпинист не проскочил ущелье на финише).


Каждую итерацию алгоритм берет старые веса, вычитает из них скорректированный на длину шага градиент и получает новые весаw^{new}, с которыми наша модель ошибается чуточку меньше.

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

Градиентный спуск для линейной регрессии

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

Дело в том, что функция потерь линейной регрессии (MSE) обладает очень хорошим свойством: она является выпуклой, а иногда (когда нет линейных зависимостей в фичах, т.е. при полном ранге матрицы признаков) — она строго выпуклая (имеет один минимум). Но, даже в случаях, когда минимумов несколько, все минимизаторы дают одно и то же значение функции потерь (все минимальные значения равны). Это значит, что в какое ущелье не попадет альпинист, там его ждут одинаковая пища/кровать и т.д.

Как и обещал, пора кодить!

Итак, на мой взгляд, мы окончательно добили линейную регрессию. Но теория без практики — удовольствие спорное. Кодим, товарищи!

Т.к. часть с кодом у нас встречается впервые, добавлю несколько пояснений.
Работать будем, как ни странно, в питоне (у меня 3.12). В первое время из библиотек нам понадобится классический джентльменский набор: numpy, pandas, scikit-learn, matplotlib и seaborn. В дальнейшем нам понадобятся еще как минимум торч и трансформеры, но до этого пока что далеко.

Примечание: для сохранения наглядности и простоты восприятия код не будет максимально коротким или оптимизированным “под прод”. Важно понимать, что учебный код пишется ради понимания сути алгоритма, а не ради экономии наносекунд процессорного времени. Тем не менее, я постараюсь сохранить базовую гигиену разработки и соблюдать основные принципы читаемости в стиле PEP 8: понятные имена переменных, аккуратные отступы и соблюдение базовых соглашений оформления кода. В целом, если вы разбираетесь в коде и вам интересна лишь математическая часть машинного обучения, можете смело пропустить этот блок.

Решаем задачу

Для начала возьмем достаточно классический датасет.

Здесь у нас восемь столбцов։

  • name — имя автомобиля

  • year — год выпуска

  • selling_price — цена (наш таргет)

  • km_driven — пробег в километрах

  • fuel — тип топлива (дизель / бензин / и т.д.)

  • seller_type — тип продавца (индивидуальный / дилер / и т.д.)

  • transmission — коробка передач

  • owner — количество владельцев

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

import pandas as pd # для работы с данными в виде таблиц
import numpy as np # для работы с массивами и числовыми операциями
import matplotlib.pyplot as plt # для визуализации данных
import seaborn as sns # опять же для визуализации (heatmap)

Далее идет загрузка данных из файла cars.csv.
Это наш датасет из kaggle, который я переименовал в cars.csv для удобства.
Также посмотрим на размер нашего датасета, и посмотрим сколько в них NaN.

Немного о NaN

В PythonNaNобычно представлен объектом numpy.nanи имеет типfloat. Вообще говоря, он не равен 0.0, поэтому код ниже выведет True:

from numpy import nan

x = nan

if x:
  print(True)

Несмотря на “утиную” философию Python, где различные “пустые” значения (0,""[]и т.д.) ведут себя одинаково в логическом контексте, сNaNтакая штука не работает.

Самое главное, не путать NaN с None (единственным объектом типа NoneType)

Самое интересное: np.nan == np.nan тоже даст False. Дело в том, что np.nan стоит воспринимать как неизвестность/неопределенность.

pd.set_option('display.float_format', '{:.2f}'.format) # просто формат отображения
df = pd.read_csv('cars.csv') 
print(df.shape)
print(df.isna().sum()) 


>>> (4340, 8)
name             0
year             0
selling_price    0
km_driven        0
fuel             0
seller_type      0
transmission     0
owner            0

Окей, 4340 строк (это то, что мы обозначили за k). Нулевое количество nan. 7 фич, один таргет.
Посмотрим содержимое. вызов df.head() показывает следующее:

первые пять строк

первые пять строк

Видим как числовые, так и категориальные признаки. Сначала разберемся с числовыми, потом придумаем что делать с категориальными.
Через df.describe().applymap(lambda x: f"{x:.2f}") видим следующее:

статистические данные о числовых признаках

статистические данные о числовых признаках

Итак, год выпуска от 1992 до 2020, цены от 20000 до почтим 9млн пробег от 1км до 800к.
Также видим средние, квантили, медианы и т.д.

Посмотрим как они коррелируют между собой.

plt.figure(figsize=(8, 6))
sns.heatmap(df[['year', 'km_driven', 'selling_price']].corr(), 
            annot=True, cmap='coolwarm', fmt='.2f')
plt.title('Correlation Matrix of Numerical Features')
plt.show()
тепловая карта (heatmap) корреляции числовых признаков

 тепловая карта (heatmap) корреляции числовых признаков

Результаты достаточно ожидаемы. Разберем по очереди

  1.  year и selling_price (Корреляция +0.41)
    Логично? Логично. Машина 2018 года выпуска при прочих равных стоит дороже, чем ее уставший аналог из 2008-го. Однако 0.41 говорит нам о том, что год, конечно же, влияет, но не определяет цену.

  2.  km_driven и selling_price (Корреляция -0.19)
    Связь слабая и отрицательная (обратная). Оказывается, пробег очень плохо объясняет цену.

  3. year и km_driven (Корреляция -0.42)
    Сильная отрицательная связь между самими признаками. Чем старше машина (меньше год), тем больше её пробег. Однако история та же, что и в пункте 1.

Одними числовыми признаками задачу нормально не решить. Но это нас не остановит и мы создадим модель линейной регрессии на 2х признаках.

Однако, до этого поговорим о такой метрике, как R^2 score. Сразу отмечу, что это не квадрат некого R, а просто обозначение. При виде R^2 = -0.100500, не надо думать что это некое комплексное число R, с квадратом, равным.

Итак, Коэффициент детерминации (или R-квардат) это обозначение величины

R^2 = 1 - \frac{\sum_{i} (y_i - \hat{y}_i)^2}{\sum_{i} (y_i - \bar{y})^2}

где
y_i — реальное значение целевой переменной дляi-го объекта.
\hat{y}_i — значение, которое предсказала модель (наш <x,w>)
\bar{y} — среднее арифметическое всех реальных значенийy (вычисляется как\bar{y} = \frac{1}{n} \sum_{i=1}^{n} y_i).

ЕслиR^2 = 1, то наша модель предсказывает всё идеально
Если 0 < R^2 < 1, то модель вполне себе рабочий (например, R^2=0.85 означает, что модель объясняет 85% вариации).
Если R^2 = 0, значит у нас получился шайтан-модель. Она предсказывает всегда просто среднее значение целевой переменной.
Если же R^2<0, то модель работает хуже, чем предсказание по среднему значению. Значит где-то что-то не так.

Теперь вернемся к делу, напишем нашу первую модель с двумя признаками и оценим полученные MSE и R^2.

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.preprocessing import StandardScaler

# выделяем целевую переменную и признаки
X = df[['year', 'km_driven']] # признаки
y = df['selling_price'] # целевая переменная

# разделяем данные на обучающую и тестовую выборки соотношением 4 к 1.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# стандартизуем признаки для улучшения качества модели 
# (год у нас в среднем 2013, а пробег в среднем 60000. 
# это может негативно влиять на обучение модели)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# создаем стандартную модель линейной регрессии и обучаем её на обучающей выборке
model = LinearRegression()
model.fit(X_train_scaled, y_train)

# делаем предсказания на тестовой выборке
y_pred = model.predict(X_test_scaled)

# оцениваем качество модели с помощью метрик MSE и R2
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f'Mean Squared Error: {mse:.2f}')
print(f'R2 Score: {r2:.2f}')

>>> Mean Squared Error: 255707328588.58
>>> R2 Score: 0.16

Модель в среднем ошибается на \sqrt{255707328588.58}. это примерно ~500 000 (валютных единиц цены автомобиля).
R^2 = 0.16 тоже так себе... Наша модель, конечно, лучше той шайтан-модели, которая всегда возвращает среднее, но успехом это не назовешь.
Однако, такой результат был вполне ожидаемым.

Так подключим категориальные признаки!
Проблема в том, что кампуктеру всё равно машина называется Lada 2101, или Lamborghini GT3. Он у нас хоть и умный, но понимает только числа. Остается одно: привести их в числовой формат.
Начнем с колонки name. df['name'].nunique() говорит, что у нас 1491 уникальных типов автомобилей.
Казалось бы, Label encoding (т.е. просто если их пронумеруем 1,2,3,...1491), решит наш вопрос. Но в реальности, он создаст другую проблему: Если Lada 2101 станет (условно) 666, Lamborghini GT3 — 1005, то как компьютер потом поймет что из них "лучше" ? Потому, отложим label encoding для тех категориальных признаков, которые сравнимы между собой.

Что насчет One Hot Encoding? В таком виде он раздует нашу таблицу до соседнего города.

One Hot Encoding (OHE)

Разберем на примере этот способ работы с категориальными признаками.
Допустим у нас в датасете есть colors в котором данные либо red, либо green, либо blue. Вместо столбца colors сделаем 3 новых: is_red, is_green, is_blue, с значениям 0/1 (True/False). Более того, is_red нам не нужен, ибо его значение получается путем
is_red =(1 - is_green)*(1 - is_blue). Тем самым у нас не возникнет лишняя корреляция, да и лишних столбцов будет меньше.

Сделаем вот что: так как в каждой строке name у нас первым делом указаны компании производящие конкретные автомобили, поменяем фичу name на фичу company.

df_company = df.copy() # создаем копию датасета, чтобы не изменять оригинальные данные
df_company['company'] = df_company['name'].apply(lambda x: x.split()[0])
df_company.drop('name', axis=1, inplace=True) # удаляем ненужный нам столбец 'name'
df_company.head()
теперь вместо конкретных моделей у нас компании производителей

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

Теперь df_company['company'].nunique() говорит, что у нас 29 уникальных компаний. У нас матрица Х имеет размеры 4340 \times 7 . Если сделаем OHE с drop_first=True и удалим столбец с названиями компаний новая матрица будет иметь размеры 4340 \times 33. Это всего 143 тысяч элементов. Для сравнения, если бы мы сделали OHE для колонки name, получили бы матрицу 4340\times1496 (около 6.5млн элементов).

Делается всё в одну строчку:
df_company = pd.get_dummies(df_company, columns=['company'], drop_first=True, dtype=int)

C первым признаком разобрались. Далее, аналогично глянем признал fuel. df['fuel'].nunique()говорит, что типов у него всего 5. Метод уже знаем, так что повторим:
df_fuel = pd.get_dummies(df_company, columns=['fuel'], drop_first=True, dtype=int)

Далее посмотрим на seller-ов. Там всего 3 уникальных значений: 'Individual', 'Dealer', 'Trustmark Dealer'. Здесь OHE тоже подходит, но, для разнообразия, поступим чуть иначе. По идее, диллеры завышают цены, частник продаст ту же машину дешевле. Попробуем вызвать df.groupby('seller_type')['selling_price'].mean()

средние цены у разных типов продавцов

средние цены у разных типов продавцов

Как видим, seller_type СИЛЬНО связан с ценой. В среднем, у Individual selling_price меньше, у Trustmark dealer — больше. Обозначим Individual за 0, Dealer за 1, Trustmark Dealer за 2.
Это немного плохо, потому что теперь у нас есть одинаковое "расстояние" между тремя типами, что вводит в небольшое заблуждение: грубо говоря, у нас теперь разница между Trustmark Dealer и Dealer эквивалентно разницы Dealer-а с Individual-ом. Более грамотным вариантом было бы внести коэффициенты хотя бы масштабируя средние, хотя и там свои подводные камны. Но всё же, для эксперимента и разнообразия, сделаем разочек LabelEncoding

mapping = {
    'Individual': 0,
    'Dealer': 1,
    'Trustmark Dealer': 2
}

df_seller = df_fuel.copy()
df_seller['seller_type'] = df_seller['seller_type'].map(mapping)

Теперь Transmisson. Там всего 2 типа: 'Manual', 'Automatic'. Берем первое за 0, второе за 1.

mapping = {
    'Manual': 0,
    'Automatic': 1
}
df_transmission = df_seller.copy()
df_transmission['transmission'] = df_transmission['transmission'].map(mapping)

Остается owner. Там дела у нас обстоят таким образом.

Что логично: чем меньше было владельцев, тем больше цена. Один к одному, как случай с продавцами. Чтож, сделаем очередной label encoding и получим финальный датафрейм с готовыми для линейной регрессии данными.

mapping = {
    'Test Drive Car': 0,
    'First Owner': 1,
    'Second Owner': 2,
    'Third Owner': 3,
    'Fourth & Above Owner': 4
    }

final_df = df_transmission.copy()
final_df['owner'] = final_df['owner'].map(mapping)

В конце у нас вышло 38 столбцов (37 фич / 1 таргет). Итоговый размер матрицы X 4340 \times 37. Примерно 160 тысяч чисел.

Сделаем теперь линейную регрессию.

from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler

X = final_df.drop('selling_price', axis=1) # признаки (все столбцы кроме 'selling_price')
y = final_df['selling_price'] # целевая переменная (столбец 'selling_price')

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, 
                                                    random_state=100500)
# сделаем линейную регрессию и обучим модель на наших данных. 
# не забудим про масштабирование данных для лучшей работы модели.

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

model = Ridge(alpha=1.0)
# alpha - это коэффициент регуляризации, 
# который контролирует степень штрафа за большие коэффициенты (гиперпараметр)
# у нас он был обозначен через лямбду.
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f'Mean Squared Error: {mse:.2f}')
print(f'R^2 Score: {r2:.2f}')
rmse = mse ** 0.5 # корень из MSE, сколько в среднем ошибались в цене
print(f'RMSE: {rmse:.2f}')

>>> Mean Squared Error: 89174913379.40
>>> R^2 Score: 0.68
>>> RMSE: 298621.69

Что можем сказать о результатах?

  • R² = 0.68 — модель объясняет около 68% изменчивости цен автомобилей. Для простой линейной модели это достаточно неплохой результат.

  • MSE ≈ 8.9 × 10^{10} — средняя квадратичная ошибка. Число большое, потому что ошибка измеряется в квадрате денежных единиц. Само по себе интерпретируется плохо.

  • RMSE ≈ 298 000 — уже более понятная метрика: модель в среднем ошибается примерно на 300 тысяч при предсказании цены автомобиля.

    По сравнению с первой версией модели качество заметно выросло: feature engineering и регуляризация дали такой эффект.

Подведем итоги

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