Вы прочитали 3 из 3 бесплатных методичек сегодня
Безлимит →
Экстракт 13 сентября 2022

Создайте алгоритм линейной регрессии с нуля на Python

Спикер канала AssemblyAI · AssemblyAI Верифицирован 17:03

Машинное обучение: реализация линейной регрессии на Python для аналитиков данных за 17 минут.

⚡ Зачем читать

  • Глубокое понимание механики: Вы перестанете воспринимать алгоритмы как «черный ящик» библиотек вроде Scikit-learn и поймете, как математика превращается в рабочий код.
  • Освоение градиентного спуска: Вы научитесь управлять процессом обучения модели, настраивая гиперпараметры для достижения минимальной ошибки.
  • Векторизация вычислений: Вы освоите использование NumPy для замены медленных циклов высокопроизводительными матричными операциями, что является стандартом в индустрии Data Science.
8 тезисов 3 задания 2 цитаты ⏱ 17 мин чтения 🎯 8 тезисов
YouTube Транскрипт Сохранить
Поделиться: TG WA VK X

Для AI-агентов и LLM

Экстракт доступен в структурированном Markdown. Скачать .md · JSON API · Site index

💡 Ключевые тезисы (8)

1 Определите линейную зависимость данных #
Сформулируйте задачу как поиск оптимальной прямой линии, максимально приближенной к точкам набора данных. Это позволит перевести задачу из визуальной плоскости в математическую формулу y = mx + b.
2 Рассчитайте среднюю квадратичную ошибку (MSE) #
Используйте формулу MSE для измерения отклонения предсказанных значений от реальных. Это даст метрику качества, которую модель сможет минимизировать в процессе обучения.
3 Примените градиентный спуск #
Вычислите градиенты функции потерь по отношению к весам и смещению. Это позволит алгоритму итеративно двигаться в сторону уменьшения ошибки, обновляя параметры модели.
4 Подберите оптимальный темп обучения (Learning Rate) #
Настройте параметр скорости обновления весов. Слишком низкий темп замедлит обучение, а слишком высокий приведет к нестабильности и невозможности найти минимум функции.
5 Векторизируйте вычисления #
Замените циклы по отдельным точкам на матричные операции с использованием NumPy. Это ускорит вычисления и упростит математические преобразования через dot-произведение.
6 Инициализируйте веса и смещение #
Установите начальные значения параметров модели в ноль перед началом итеративного процесса обучения. Это стандартная точка отсчета для большинства градиентных методов оптимизации.
7 Реализуйте класс модели на Python #
Инкапсулируйте логику обучения и предсказания в объектно-ориентированную структуру. Создайте методы .fit() для обучения и .predict() для инференса на новых данных.
8 Протестируйте модель на сгенерированных данных #
Используйте библиотеку scikit-learn для создания тестового датасета. Это позволит верифицировать точность алгоритма и визуализировать результат в виде линии регрессии.

Пособие: Реализация линейной регрессии с нуля на Python

🗺 Карта навыков

Этап Навык Инструменты
1 Формализация задачи Математическая статистика, Линейная алгебра
2 Реализация модели Python, ООП, NumPy
3 Оптимизация Градиентный спуск, MSE, Learning Rate
4 Инференс и оценка Scikit-learn, Matplotlib

1. Определите линейную зависимость данных

Линейная регрессия — это фундамент машинного обучения, с которого начинается путь любого аналитика. Основная цель метода заключается в поиске такой прямой линии, которая наилучшим образом описывает взаимосвязь между входными признаками (x) и целевой переменной (y). В контексте рассмотренного видео, спикер подчеркивает, что мы стремимся найти оптимальные параметры — вес (weight) и смещение (bias) — для уравнения вида y = mx + b. Когда мы работаем с набором данных, мы предполагаем наличие линейного паттерна. Это означает, что наши точки на графике в идеале должны группироваться вокруг прямой линии.

В процессе реализации мы переводим визуальную задачу «проведения линии» в математическую формулу. Спикер отмечает: «What we try to do here is to calculate or estimate this equation that would give us a linear line that would fit this data the best to calculate the error of this line we use mean squared error». Эти слова подтверждают, что визуальное соответствие — это лишь начало. Чтобы алгоритм «понял», что линия проведена верно, нам нужно количественно измерить отклонение каждой точки от этой прямой.

Представьте, что у вас есть набор точек, где x — это количество часов обучения, а y — оценка на экзамене. Линейная регрессия пытается найти такой коэффициент наклона (вес), чтобы при подстановке любого значения x мы получали прогноз, максимально близкий к реальному баллу. Если веса инициализированы неверно, линия пройдет «мимо» большинства точек. Наша задача — итеративно приближать эту линию к данным, минимизируя среднюю квадратичную ошибку (MSE). Этот процесс обучения превращает случайную линию в точный прогностический инструмент. Понимание этой зависимости критически важно, так как все последующие алгоритмы, даже глубокие нейронные сети, строятся на аналогичных принципах оптимизации весов. Вы учитесь управлять «наклоном» и «сдвигом» функции, чтобы она «выучила» скрытую логику ваших данных.

«With linear regression what we're trying to do is to understand the pattern or the slope of a given data set and the assumption that we're making is that this data set has a linear pattern so here what we try to do is given this data set draw a linear line that fits this data as well as possible.»

Сделайте сейчас: Создайте простой массив данных в Python с помощью библиотеки numpy. Создайте x как массив чисел от 1 до 10 и y как 2 * x + 1. Попробуйте вручную задать веса w=1 и b=0, вычислите предсказания y_pred и сравните их с реальными значениями y. Это даст вам первое понимание того, как параметры модели влияют на точность прогноза.

2. Рассчитайте среднюю квадратичную ошибку (MSE)

После того как мы задали уравнение линии, нам необходим измерительный прибор — функция потерь. В линейной регрессии чаще всего используется Mean Squared Error (MSE), или средняя квадратичная ошибка. Логика здесь проста: мы берем разность между реальным значением и предсказанным, возводим её в квадрат, чтобы избавиться от отрицательных значений (ведь ошибка в -5 и +5 должна трактоваться одинаково как «плохо»), суммируем эти квадраты для всех точек и делим на их количество. Это число и является нашим «компасом».

В видео спикер подробно объясняет, что именно MSE позволяет нам понять, насколько модель далека от идеала. Когда мы вычисляем MSE, мы получаем конкретную метрику: чем она меньше, тем лучше наша прямая «сидит» на данных. Спикер говорит: «To find the best fitting line we need to find the values for the parameters of our model so weight and the bias that will give us the minimum mean squared error». Это ключевой момент для аналитика данных. Вы не просто подбираете параметры «на глаз», а создаете вычислительную среду, где модель сама стремится к минимуму этой функции.

Если вы представите график зависимости MSE от значения веса, вы увидите форму чаши (параболу). Цель алгоритма — скатиться с края этой чаши в самую низкую её точку (минимум). Для этого нам нужно знать, в какую сторону «спускаться», что достигается через вычисление производных (градиентов) функции MSE по отношению к весам. Спикер подчеркивает важность этого шага: «To do that we need to calculate the derivative or the gradient of mean squared error and this is how it is calculated to do this optimization we use a technique called gradient descent». Без понимания того, как ошибка зависит от весов, обучение невозможно. В векторизованном виде, который предлагает автор видео, расчет ошибки происходит сразу для всего массива данных, что многократно ускоряет обучение по сравнению с перебором каждой точки по отдельности. Вычисление MSE в матричном виде с помощью numpy — это стандарт индустрии, который позволяет работать с датасетами любого размера, оставаясь в рамках эффективных вычислений.

«And that is calculated as the actual value of a data point minus the estimated and then we get a square we do this for all the data points in our data set and then we divide this value by the number of data points in our data sets to find the best fitting line.»

Сделайте сейчас: Напишите функцию mean_squared_error(y_true, y_pred) на Python, используя numpy. Проверьте её работу на двух массивах: реальных значениях [1, 2, 3] и ваших предсказаниях [1.1, 1.9, 3.2]. Убедитесь, что функция возвращает скалярное число, которое отражает среднюю квадратичную ошибку вашей модели на этом мини-примере.


3. Примените градиентный спуск

Градиентный спуск — это «двигатель» вашей модели, без которого она оставалась бы статичным уравнением с неопределенными параметрами. Мы уже выяснили, как измерить ошибку (MSE), но теперь нам нужно понять, как именно «подкрутить» веса (weight) и смещение (bias), чтобы эта ошибка уменьшалась с каждой итерацией. В видео спикер объясняет этот процесс как поиск пути вниз по склону холма. Градиент — это вектор производных, который указывает направление самого крутого подъема. Поскольку мы хотим минимизировать функцию потерь, мы делаем шаг в противоположном направлении, то есть «спускаемся».

В процессе реализации мы вычисляем градиенты функции потерь относительно каждого параметра. Спикер отмечает: «What we do with gradient descent is we calculate at a point where we have our parameter value which direction to go using the derivatives of the cost function to minimize the mean squared error». Математически это выглядит как обновление параметров: w = w - learning_rate * dw и b = b - learning_rate * db, где dw и db — частные производные по весу и смещению соответственно. Это итеративный процесс, который повторяется сотни или тысячи раз.

Представьте, что вы стоите в тумане на вершине горы и хотите спуститься в долину. Вы не видите всей карты, но можете почувствовать наклон почвы под ногами. Направление, в котором уклон самый крутой вниз, — это и есть наш антиградиент. Сделав шаг в этом направлении, вы снова проверяете наклон. Так, шаг за шагом, модель «скатывается» к оптимальному значению параметров. Важно понимать, что градиентный спуск — это не магия, а строгое следование математическому анализу. Если веса инициализированы нулями, как в нашем коде, то в первой итерации модель делает «первый шаг» в сторону уменьшения ошибки. Важно отметить, что в векторизованном подходе мы обновляем веса для всех признаков одновременно, что делает алгоритм чрезвычайно эффективным даже при работе с большими массивами данных. Это фундаментальный механизм обучения, который лежит в основе не только линейной регрессии, но и архитектур современных нейросетей.

«What we do with gradient descent is we calculate at a point where we have our parameter value which direction to go using the derivatives of the cost function to minimize the mean squared error or generally the error of the model.»

Сделайте сейчас: Реализуйте в своем коде метод обновления параметров. Добавьте в цикл обучения (который работает n итераций) обновление self.weights и self.bias на основе градиентов dw и db, используя заранее заданный learning_rate. Попробуйте изменить количество итераций (например, с 1000 на 5000) и посмотрите, как изменится значение MSE. Это позволит вам увидеть, как количество «шагов» влияет на достижение глобального минимума.

4. Подберите оптимальный темп обучения (Learning Rate)

Темп обучения, или Learning Rate (LR), — это критически важный гиперпараметр, определяющий «длину шага» при спуске по поверхности функции потерь. Если градиентный спуск определяет направление движения, то LR определяет, насколько далеко мы уйдем в этом направлении за один раз. В видео спикер наглядно показывает, что выбор этого значения — это поиск баланса между скоростью обучения и стабильностью процесса. Если установить слишком маленький темп обучения, модель будет «ползти» к минимуму бесконечно долго, что может привести к неэффективным расходам вычислительных ресурсов. С другой стороны, слишком высокий LR приведет к тому, что алгоритм будет «перепрыгивать» через минимум, вызывая осцилляции (колебания) и, в худшем случае, расхождение градиента, при котором ошибка будет только расти.

Спикер акцентирует внимание на этой проблеме: «If your learning rate is too low it might cause a problem of you not achieving the minimum error because you're approaching it very slowly if your learning rate is too high you might keep jumping around in your airspace and you might never be able to find the minimum». Это наблюдение подчеркивает, что подбор правильного LR — это не просто техническая деталь, а полноценная часть настройки модели. В своей практике вы часто будете сталкиваться с необходимостью экспериментировать: начать с 0.001, протестировать 0.01 или даже меньшие значения в зависимости от масштаба ваших данных.

В контексте линейной регрессии, когда мы работаем с матрицами, важно учитывать, что ошибки в расчетах из-за «слишком больших шагов» могут моментально привести к появлению NaN (Not a Number) в весах. Это происходит, когда значения весов улетают в бесконечность. Умение диагностировать поведение модели по графику MSE (он должен плавно падать, а не дергаться вверх-вниз) — навык, который отделяет новичка от опытного аналитика. Понимание того, как темп обучения взаимодействует с градиентами, позволяет вам более осознанно подходить к оптимизации любых моделей машинного обучения, где используется градиентный спуск.

«The learning rate tells us how fast or slow to go in the direction that gradient descent tells us to go so for example if your learning rate is too low it might cause a problem of you not achieving the minimum error because you're approaching it very slowly.»


5. Векторизируйте вычисления с помощью NumPy

В процессе обучения модели линейной регрессии крайне важно перейти от пошаговой обработки каждого примера из датасета к матричным операциям. Если вы будете использовать циклы for для перебора каждой точки данных (например, для вычисления прогноза y = wx + b), ваш код будет работать катастрофически медленно при увеличении объема данных. Спикер в видео делает особый акцент на использовании NumPy для векторизации, утверждая, что это стандартный и эффективный способ работы с данными в индустрии. Векторизация позволяет выполнять математические операции сразу над целыми массивами данных, используя оптимизированные внутренние механизмы библиотеки, написанные на C.

Когда мы говорим о векторизации, мы заменяем скалярные умножения на матричное произведение (dot product). В видео спикер поясняет: «we're not going to go over the samples or the data points one by one we're going to put them in a matrix and we're going to do them all together all the calculations at the same time». Это означает, что если у нас есть матрица X размера (N x D), где N — количество образцов, а D — количество признаков, мы умножаем её на вектор весов W размера (D x 1). Результатом будет вектор предсказаний y_pred размером (N x 1). Такой подход превращает громоздкие итерации в элегантную запись np.dot(X, self.weights) + self.bias, что не только быстрее, но и гораздо ближе к математической нотации, принятой в научной литературе по Data Science.

Более того, векторизация значительно упрощает расчет градиентов. Вместо того чтобы писать сложные суммы для производных, мы можем выразить их через транспонирование матриц. Спикер отмечает, что использование X.T (транспонированной матрицы X) при вычислении dw позволяет корректно сопоставить размерности и получить градиент для всех весов одновременно. Если вы когда-либо сталкивались с ошибками размерности (Dimension Mismatch) в Python, знайте, что именно понимание того, как данные «укладываются» в матрицы, решает 99% этих проблем. Освоение numpy.dot и атрибута .shape делает вас профессионалом, способным масштабировать алгоритмы на реальные, «живые» данные, а не только на учебные примеры.

«When we're calculating the gradients it's actually going to make it a little bit easier for us because now we can get all the values of x all the data points transpose this matrix so that they're column and then we can multiply this or we can take the dot product of this with the y predicted but of course it's going to be the difference between the y predicted and the actual y.»

Сделайте сейчас: Перепишите метод predict в вашем классе, используя np.dot. Убедитесь, что вы правильно обрабатываете размерности: если X имеет форму (n_samples, n_features), а weights(n_features,), результат должен быть вектором (n_samples,). Протестируйте это на массиве X = np.array([[1, 2], [3, 4]]) и weights = np.array([0.5, 0.5]), добавив смещение bias = 1. Вы должны получить предсказания для обоих объектов за одну операцию.

6. Инициализируйте веса и создайте структуру класса

Структура программы — это фундамент, от которого зависит, насколько легко модель будет интегрироваться в реальные аналитические пайплайны. Спикер предлагает инкапсулировать логику линейной регрессии в класс, что позволяет нам использовать стандартизированный подход fit (обучение) и predict (предсказание). В методе __init__ мы задаем гиперпараметры, такие как learning_rate и n_iterations, а также готовим «место» для весов и смещения. Важный нюанс: веса не задаются заранее, так как их размерность зависит от количества признаков в данных, которые мы узнаем только в момент вызова метода fit.

Инициализация весов нулями — это классический подход для линейной регрессии, который хорошо работает, так как функция потерь (MSE) здесь является выпуклой, и алгоритм всегда сходится к глобальному минимуму, независимо от начальной точки. Спикер подчеркивает этот момент: «during training we initialize the weight and the bias as zero and then given a data point we predict the result or estimate the result with this given equation». В методе fit мы первым делом определяем количество признаков через n_samples, n_features = X.shape, что дает нам возможность динамически создать массив self.weights = np.zeros(n_features). Это делает ваш код универсальным: он будет работать как с одним признаком, так и с сотней.

Создание полноценного класса также приучает вас к хорошему тону в программировании: вы отделяете процесс настройки модели от процесса её применения. Метод fit выполняет тяжелую работу — итеративное обновление весов через градиентный спуск, в то время как predict — это лишь быстрое применение выученных параметров к новым данным. Спикер также упоминает, что при возникновении ошибок размерности (как в случае с забытым транспонированием), объектно-ориентированный подход позволяет легко отлаживать состояние модели, так как все переменные (self.weights, self.bias) хранятся внутри экземпляра класса. Вы можете в любой момент вывести их на экран и увидеть, «выучила» ли модель параметры или веса остались нулевыми.

«I'm going to again define it as a class and our in our initialization function we're going to have a bunch of things so first we need a learning rate just call it lr and i'm going to pass a default lr let's make this 0.001 maybe for now.»

Сделайте сейчас: Оформите ваш код в класс LinearRegression с методами __init__, fit и predict. Внутри fit реализуйте цикл for на self.n_iterations итераций, внутри которого вызывается обновление параметров. После обучения выведите значения self.weights и self.bias. Попробуйте инициализировать веса не нулями, а маленькими случайными числами через np.random.rand(n_features) — сравните, как быстро модель сходится к оптимальному результату по сравнению с нулевой инициализацией.


7. Реализуйте и протестируйте модель на синтетических данных

Когда мы завершили архитектуру класса, пришло время проверки боем. Мало просто написать код, нужно убедиться, что он работает на данных, структура которых нам известна. Спикер предлагает использовать библиотеку scikit-learn для генерации синтетического датасета. Это отличная практика, так как вы получаете «чистые» данные, где связь между признаками и целевой переменной задана заранее. В процессе тестирования вы увидите, как ваша модель пытается найти ту самую «идеальную» прямую, которую мы заложили при генерации. Использование sklearn.datasets.make_regression позволяет гибко настраивать количество шума, количество признаков и масштаб данных, что делает ваш тест максимально приближенным к реальности.

В видео спикер демонстрирует процесс: «I'm going to make a regression using a scikit-learn's data set feature and let's see what this data set looks like on a plot». Важно не просто запустить модель, а визуализировать результат с помощью библиотек типа matplotlib. Когда вы видите график с точками данных и наложенной на них линией регрессии, вы понимаете, как веса (weights) и смещение (bias) превращаются в геометрический наклон и точку пересечения с осью Y. Если линия проходит мимо «облака» точек, это сигнал к тому, что нужно пересмотреть гиперпараметры или проверить логику обучения.

Постепенная отладка — ключ к успеху. В видео спикер сталкивается с Dimension Mismatch из-за неправильной транспозиции матрицы признаков X. Это классическая ошибка: когда размерность весов не совпадает с количеством признаков в строках матрицы. Спикер делает вывод: «all I need to do actually here is just say get the transpose of x and now it should work». Это учит нас тому, что в Data Science ошибки размерностей — не признак вашей некомпетентности, а стандартная часть рабочего процесса, которую нужно уметь диагностировать через метод .shape.

«It looks like we have a really nice linear line that really captures the slope of this data set so that's all from linear regression if you'd like this code you can go get it in our github repository.»

Сделайте сейчас: Сгенерируйте данные с помощью make_regression(n_samples=100, n_features=1, noise=20). Обучите ваш класс LinearRegression. Постройте график, используя plt.scatter для данных и plt.plot для предсказаний. Измените параметр noise в генераторе и посмотрите, как это влияет на MSE и на то, насколько «дерганой» становится итоговая линия регрессии.

8. Итеративный процесс улучшения и финальная оценка

Финальный этап разработки модели — это настройка «под ключ». После того как вы реализовали алгоритм, важно научиться интерпретировать метрику качества. Среднеквадратичная ошибка (MSE) — это ваш главный компас. Спикер показывает, как изменение одной цифры — learning_rate — драматически меняет результат. При переходе от 0.001 к более высокому значению (например, 0.01), модель может внезапно «попасть» в минимум, что выражается в резком снижении MSE с 783 до 305. Это наглядная демонстрация того, что аналитик — это исследователь, который постоянно проверяет гипотезы.

Спикер отмечает: «Maybe if I make the learning rate a bit higher maybe a bit bigger maybe that will help». Этот поиск «золотой середины» — основа работы ML-инженера. Важно понимать, что каждое обучение модели — это цикл. Вы задаете параметры, запускаете fit, смотрите на график MSE (он должен монотонно убывать), оцениваете результат через predict и, если результат неудовлетворителен, меняете гиперпараметры. Это и есть та самая «магия» машинного обучения, где из хаотичных чисел рождается стройная математическая модель.

В завершение работы над моделью, подумайте о том, как ваш код будет использоваться в будущем. Чистый, хорошо задокументированный код с понятными именами переменных (y_predicted, n_samples, lr) позволяет легко вносить изменения. Если вы захотите добавить регуляризацию, расширить модель до полиномиальной или добавить расчет других метрик (например, R-squared), наличие прочного фундамента в виде класса LinearRegression спасет вас от необходимости переписывать все с нуля.

«Maybe if I make the learning rate a bit higher maybe a bit bigger maybe that will help let's try okay so that's much better our mean squared error is 305.8 and this is what our graph looks like.»

Сделайте сейчас: Реализуйте функцию get_mse(y_true, y_pred), которая принимает два массива и возвращает среднее квадратичное отклонение. Прогоните обучение вашего класса 500, 1000 и 5000 итераций. Запишите, как меняется MSE. Постройте «кривую обучения» (Learning Curve), где по оси X — количество итераций, а по Y — значение MSE, чтобы увидеть момент, когда модель перестает обучаться (плато).

🏋️ Практикум

  1. Напишите функцию mean_squared_error вручную на NumPy, не используя встроенные функции библиотек для вычисления разности.
  2. Реализуйте «early stopping»: остановите цикл обучения внутри fit, если ошибка (MSE) за последние 10 итераций уменьшилась менее чем на 0.001.
  3. Добавьте в класс LinearRegression параметр fit_intercept (булево значение). Если False, то смещение bias должно оставаться 0 и не обновляться.
  4. Протестируйте модель на данных с 2 и 3 признаками, убедившись, что weights становятся вектором соответствующей размерности.
  5. Создайте визуализацию «траектории» весов: сохраняйте значения весов на каждой итерации и выведите график, как они менялись во времени до стабилизации.
  6. Добавьте проверку на NaN в весах: если в процессе обучения появляются бесконечные значения, выводите предупреждение «Learning rate too high!» и останавливайте обучение.
🔒

Бесплатный лимит исчерпан

Вы прочитали 3 методичек сегодня. Завтра лимит обновится, или подпишитесь для неограниченного доступа.

🏋️ Практикум

0 / 3 выполнено

Настройка класса LinearRegression

⏱ 20 мин 🎯 Цель: Создать структуру класса с методами fit и predict. Шаги: 1. Объявите класс с параметрами learning_rate и iterations. 2. Реализуйте метод fit для инициализации весов нулями. 3. Напишите метод predict для вычисления результата. ✅ Результат: Файл linear_regression.py.

Оптимизация через векторизацию

⏱ 15 мин 🎯 Цель: Избежать циклов for при расчете градиентов. Шаги: 1. Замените скалярные операции на numpy.dot(). 2. Примените транспонирование матрицы X. 3. Рассчитайте градиенты по формуле (1/n) * X.T * (y_pred - y). ✅ Результат: Оптимизированный код обучения.

Тюнинг гиперпараметров

⏱ 10 мин 🎯 Цель: Снизить значение MSE через подбор learning rate. Шаги: 1. Запустите модель с lr=0.001. 2. Постепенно увеличивайте значение. 3. Замерьте итоговую ошибку MSE. ✅ Результат: Отчет с лучшим показателем MSE.
🎉
Все задания выполнены!
Отлично — знания превращены в навыки

💬 Цитаты (2)

«Линейная регрессия пытается найти закономерность или наклон данных, предполагая наличие линейной зависимости. Мы стремимся провести линию, которая лучше всего описывает этот набор данных.» #

Определяет фундаментальную цель обучения модели.

«Скорость обучения говорит нам о том, как быстро или медленно двигаться в направлении, указанном градиентным спуском. Если она слишком мала, мы не достигнем минимума; если слишком велика — будем постоянно перепрыгивать через него.» #

Объясняет важность выбора правильного гиперпараметра обучения.

Читать далее

Освойте полный цикл машинного обучения: от подготовки данных до настройки моделей на Python

AssemblyAI

Освойте полный цикл машинного обучения: от подготовки данных до настройки моделей на Python

Спикер AssemblyAI

Понравился экстракт?
Подписывайтесь — лучшие материалы каждую неделю.
Telegram Дайджест →

Поделитесь с коллегами

Telegram ВКонтакте X / Twitter
Открыть в Telegram

Экстракт Знаний в Telegram

Экстракты и дистилляты из лучших YouTube-каналов — сразу после публикации.

Подписаться

Дайджест Экстрактов

Лучшие методички за неделю — каждый понедельник