ПЕРЕКРЕСТНАЯ ПРОВЕРКА

Темы, которые будут освещены в этом блоге:

1. Необходимость перекрестной проверки

2. Удерживай подход / Тестовый сплит

3. ПЕРЕКРЕСТНАЯ ПРОВЕРКА

4. Исключить одну перекрестную проверку (LOOCV)

5. Перекрестная проверка K-fold

6. Многослойное резюме в K-сгибе

Необходимость перекрестной проверки -

  • В машинном обучении нам приходится в основном работать с табличными наборами данных (в случае контролируемого машинного обучения) и обучать модель на данных. Как мы можем гарантировать, что эта обученная модель будет работать с «новым набором данных»?
  • Мы не можем рисковать развертыванием модели, пока не обеспечим ее работоспособность. Вот почему нам нужна «Перекрестная проверка», чтобы мы могли оценить модель.

Удерживай подход / Тестовое разделение поезда -

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

Но это не единственный путь.

Существует улучшенный способ применения метода оценки модели, который называется “Cross-validation”.

Проблемы с подходом удержания:

  1. Вариативность –
  • Самая большая проблема заключается в том, что “variability” поскольку мы сначала перемешиваем данные и делим их на две части, поэтому мы не знаем, какая строка попадет в обучающий набор, а какая строка в тестовый набор.
  • “Random State” — это параметр, который мы используем при перетасовке данных, чтобы каждый раз получать один и тот же результат, но если мы изменяем случайное состояние, точность модели может варьироваться для каждого случайного состояния. Таким образом, мы можем сказать, что точность, которую мы получаем от модели, ненадежна из-за высокой дисперсии.
  • Пример: в Бостонских жилищных данных я разделил данные с разными случайными состояниями, обучил регрессионную модель находить оценку R2 и получил 3 разных оценки R2. Как мы можем убедиться, какая оценка R2 дает лучший результат?

Random_state — (1,2,3)

R2_Score — (0.76, 0.77, 0.79)

2.Неэффективность данных –

  • Метод HoldOut использует только часть данных для обучения модели, поскольку мы обычно используем 70 или 80 данных для обучения модели вместо 100-процентного использования данных. Итак, мы прекрасно знаем “MORE DATA GIVE BETTER MODEL”, но здесь мы не используем все данные.

3. Предвзятость в оценке эффективности –

  • В балансе между предвзятостью и дисперсией мы разбиваем «Потери» на три части: “Bias,” “Variance,” and “Noise.” Давайте разберемся в этих понятиях:

Представьте себе игрока в гольф, стремящегося попасть в яблочко. Однако среднее место приземления их мяча постоянно находится далеко от цели. Эту разницу между целью игрока в гольф и фактическим результатом мы называем «предвзятостью». Это все равно, что иметь личное мнение о том, куда должен лететь мяч, без полного понимания данных. По мере того как мы уменьшаем количество имеющихся у нас данных, эта предвзятость имеет тенденцию увеличиваться.

Почему мы используем подход «Удержание»?

  1. Простота –
  • Подход «Удержание» — straightforward и easy to understand. Просто разделите данные на две части. Это хорошо, когда нам нужно поработать над примерами проектов или провести первоначальный исследовательский анализ.

2. Вычислительная эффективность –

  • Это на less intensive лучше, чем такие методы, как “K-fold Cross-validation”. Здесь просто нужно обучить модель один раз, а не повторять обучение типа K раз.

3. Большие наборы данных –

  • Для очень большого набора данных высокая дисперсия начнет устраняться, даже если мы изменим случайное состояние.
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error,r2_score
import numpy as np
import pandas as pd

df = pd.read_csv('https://raw.githubusercontent.com/selva86/datasets/master/BostonHousing.csv')
# Splitting the data into train and test set

X_train, X_test, y_train, y_test = train_test_split(df.iloc[:,0:-1], df.iloc[:,-1], test_size = 0.2, random_state = 1)
# Create a linear Regression model

model = LinearRegression()

# Train the Model on Training data
model.fit(X_train, y_train)

# Use the Trained model to predict the Target values in Test set
y_pred = model.predict(X_test)
# Calculate the Mean Squared Error of the model on the test set
mse = mean_squared_error(y_test, y_pred)

print('Mean Squared Error:', mse)

r2 = r2_score(y_test, y_pred)

print('r2 score:', r2)

Среднеквадратическая ошибка: 23.380836480270336

Оценка r2: 0,7634174432138461

# Now will again split the dataset with random state 2 to check model r2 score and MSE  will improve or not

X_train, X_test, y_train, y_test = train_test_split(df.iloc[:,0:-1], df.iloc[:,-1], test_size = 0.2, random_state = 2)
# Create a linear Regression model

model = LinearRegression()

# Train the Model on Training data
model.fit(X_train, y_train)

# Use the Trained model to predict the Target values in Test set
y_pred = model.predict(X_test)
# Calculate the Mean Squared Error of the model on the test set
mse = mean_squared_error(y_test, y_pred)

print('Mean Squared Error:', mse)

r2 = r2_score(y_test, y_pred)

print('r2 score:', r2)

Среднеквадратическая ошибка: 18.49542012244846

Оценка r2: 0,7789207451814409

Итак, мы видим, что при изменении состояния Random мы получаем different r2Score и MSE , что является явным случаем High Variance.

Теперь мы будем работать с “MNIST DATASET” с различными случайными состояниями, поскольку это большие данные, поэтому в большинстве случаев мы не получаем изменений в результатах точности. Как мы знаем, проблема дисперсии больших наборов данных уменьшается.

from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score

# Load the MNIST dataset
mnist = fetch_openml('mnist_784', version=1)
X, y = mnist.data, mnist.target

# Split the dataset into a training set and a test set
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1)

model = DecisionTreeClassifier()

# Train the model on the training data
model.fit(X_train, y_train)

# Use the trained model to predict the target values in the test set
y_pred_test = model.predict(X_test)

# Calculate the accuracy of the model on the training and test sets
accuracy_test = accuracy_score(y_test, y_pred_test)

print('Test Accuracy:', accuracy_test)
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score

# Load the MNIST dataset
mnist = fetch_openml('mnist_784', version=1)
X, y = mnist.data, mnist.target

# Split the dataset into a training set and a test set
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=2)

model = DecisionTreeClassifier()

# Train the model on the training data
model.fit(X_train, y_train)

# Use the trained model to predict the target values in the test set
y_pred_test = model.predict(X_test)

# Calculate the accuracy of the model on the training and test sets
accuracy_test = accuracy_score(y_test, y_pred_test)

print('Test Accuracy:', accuracy_test)

Результат точности для случайного состояния 1–0,86

Результат точности со случайным состоянием 2–0,87

ПЕРЕКРЕСТНАЯ ПРОВЕРКА

  • Идея перекрестной проверки состоит в том, чтобы разделить данные на несколько подмножеств или “folds”. Затем модель обучается на некоторых из этих подмножеств и тестируется на остальных. Этот процесс повторяется несколько раз, каждый раз для обучения и проверки используются разные подмножества. Результаты каждого раунда обычно усредняются для оценки общей эффективности модели.

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

  • Resampling comes from Sampling, а выборка — это, по сути, процесс, в котором мы извлекаем некоторые наблюдения из совокупности, чтобы оценить статистику, связанную с совокупностью.

Повторная выборка — это серия методов, используемых в статистике для сбора дополнительной информации о выборке. Это может включать повторный отбор пробы или оценку ее точности. Предположим, что в наборе данных IRIS, который представляет собой образец набора данных из 150 цветов, мы создаем больше образцов из 100, 100 цветов и обучаемую модель для всех образцов. Этот метод называется повторной выборкой.

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

Оставить одно резюме (LOOCV)

Покинуть точку резюме

2. K- FOLD CV:

К- сгиб,

стратифицированный,

Вложенный K-фолд,

Повторный

Исключить одну перекрестную проверку (LOOCV)

  • Эта техника LOOCV формирует model equal to the number of datasets, которые у нас есть. Если у нас есть 1000 строк, в наборе данных будет сформировано 1000 моделей для оценки модели.
  • Теперь создадим новый набор данных, удалив первую строку и взяв n-1 строку в этом примере (999 строк). Это называется «Обучающий набор данных», а «Первая строка» называется «Тестовый набор данных», и в этом случае мы обученную «Модель 1».
  • Теперь возьмем вторую строку в качестве тестового набора данных, а еще 999 строк будут использоваться для построения модели.
  • Таким образом, мы выполняем этот процесс 1000 раз в заданном наборе данных из 1000 строк, и у нас будет 1000 моделей, и мы получим точность для всех 1000 моделей, поэтому мы получим среднее значение точности 1000, и это среднее значение станет «Окончательным показателем точности». весь процесс «LOOCV»
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import LeaveOneOut, cross_val_score

# Load the Boston Housing dataset
df = pd.read_csv('https://raw.githubusercontent.com/selva86/datasets/master/BostonHousing.csv')
X = df.iloc[:,0:-1]
y = df.iloc[:,-1]

# Create a linear regression model
model = LinearRegression()

# Create a LeaveOneOut cross-validator
loo = LeaveOneOut()

# Use cross_val_score for the dataset with the model and LOOCV
# This will return the scores for each iteration of LOOCV
scores = cross_val_score(model, X, y, cv=loo, scoring='neg_mean_squared_error')

# neg mean squared error as we want to reduce mean squared error

mse_scores = -scores  # Invert the sign of the scores as taken negative mean square error 

# Print the mean MSE over all LOOCV iterations
print("Mean MSE:", mse_scores.mean())

Среднее MSE: 23,72574551947613

X.shape
# 506 model formed and every model have MSE, so when calculate Mean of all MSE get "23"

(506, 13)

Преимущества LOOCV -

  1. Использование данных:
  • В LOOCV мы почти используем entire dataset, который будет reduce bias.
  • LOOCV использует почти все данные для обучения, что может быть полезно в ситуациях, когда набор данных невелик и каждая точка данных ценна.
  • Поскольку каждая итерация проверки выполняется только для одной точки данных, LOOCV менее предвзят, чем другие методы, такие как K-кратная перекрестная проверка. Процесс проверки меньше зависит от случайного разделения данных.

2. Никакой случайности:

  • Здесь нет перетасовки, как в методе удержания. В простом порядке мы create different sets и обучили модели without any randomness.

Недостатки LOOCV-

  1. Вычислительные затраты:
  • LOOCV требует подгонки модели N раз, что может быть дорогостоящим в вычислительном отношении и отнимать много времени для больших наборов данных.

2. Высокая дисперсия:

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

3. Неподходящий показатель эффективности:

  • Мы не можем использовать оценку r2 в LOOCV, поскольку при оценке мы не можем усреднить оценки для всех моделей. R2score не определяется, если набор проверки содержит только один образец.

4. Не идеально подходит для несбалансированного набора данных:

  • Предположим, что для задачи классификации у нас есть 1000 наборов данных и 990 данных с «да» и 10 данных с «Нет», так что это явный случай несбалансированного набора данных, который не может быть обработан ни одной моделью.

Когда использовать LOOCV –

  • Когда у нас есть “Small Datasets”, и “Balanced Datasets”,, нам также нужен “Less Biased Performance Estimates”
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import KFold
import pandas as pd

# Load the Boston Housing dataset
df = pd.read_csv('https://raw.githubusercontent.com/selva86/datasets/master/BostonHousing.csv')
X = df.iloc[:,0:-1]
y = df.iloc[:,-1]

# Initialize a Linear Regression model
model = LinearRegression()

# Initialize the KFold parameters
kfold = KFold(n_splits=10, shuffle=True, random_state=42)

# Use cross_val_score on the model and dataset
scores = cross_val_score(model, X, y, cv=kfold, scoring='r2')

print("R2 scores for each fold:", scores)
print("Mean R2 score across all folds:", scores.mean())
Applying Degree of Polynomial with single Random state on LOOCV Technique
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import mean_squared_error

# Load the Auto MPG dataset
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data"
column_names = ['MPG', 'Cylinders', 'Displacement', 'Horsepower', 'Weight', 'Acceleration', 'Model Year', 'Origin']
auto_mpg = pd.read_csv(url, names=column_names, delim_whitespace=True, na_values='?')

# Drop rows with missing values
auto_mpg = auto_mpg.dropna()

# Define features and target
features = auto_mpg[['Horsepower']]  # Only use 'Horsepower' as a feature
target = auto_mpg['MPG']

# List of polynomial degrees
degrees = list(range(1, 11))  # Extend degrees to 10

# Split the dataset into a training set and a test set
X_train, X_test, y_train, y_test = train_test_split(features, target, test_size=0.2, random_state=4)

# List to store Mean Squared Errors
mse_list = []

for degree in degrees:
    # Add polynomial features
    poly = PolynomialFeatures(degree=degree)
    X_train_poly = poly.fit_transform(X_train)
    X_test_poly = poly.transform(X_test)

    # Create a linear regression model
    model = LinearRegression()

    # Train the model on the polynomial features training data
    model.fit(X_train_poly, y_train)

    # Use the trained model to predict the target values in the test set
    y_pred = model.predict(X_test_poly)

    # Calculate the Mean Squared Error of the model on the test set
    mse = mean_squared_error(y_test, y_pred)

    # Store the Mean Squared Error in the list
    mse_list.append(mse)

# Plot the Mean Squared Error as a function of the degree of the polynomial
plt.figure(figsize=(10, 6))
plt.plot(degrees, mse_list, marker='o')
plt.title('Mean Squared Error vs. Degree of Polynomial')
plt.xlabel('Degree of Polynomial')
plt.ylabel('Mean Squared Error')
plt.grid(True)
plt.show()

In Graph can see on degree of 4 polynomial MSE is minimum
Applying Degree of Polynomial with different random state using LOOCV Technique
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import mean_squared_error

# Load the Auto MPG dataset
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data"
column_names = ['MPG', 'Cylinders', 'Displacement', 'Horsepower', 'Weight', 'Acceleration', 'Model Year', 'Origin']
auto_mpg = pd.read_csv(url, names=column_names, delim_whitespace=True, na_values='?')

# Drop rows with missing values
auto_mpg = auto_mpg.dropna()

# Define features and target
features = auto_mpg[['Horsepower']]  # Only use 'Horsepower' as a feature
target = auto_mpg['MPG']

# List of polynomial degrees
degrees = list(range(1, 11))  # Extend degrees to 10

# List of random states
random_states = list(range(10))

# Create a plot
plt.figure(figsize=(10, 6))

for i, random_state in enumerate(random_states):
    # Split the dataset into a training set and a test set
    X_train, X_test, y_train, y_test = train_test_split(features, target, test_size=0.2, random_state=random_state)

    # List to store Mean Squared Errors
    mse_list = []

    for degree in degrees:
        # Add polynomial features
        poly = PolynomialFeatures(degree=degree)
        X_train_poly = poly.fit_transform(X_train)
        X_test_poly = poly.transform(X_test)

        # Create a linear regression model
        model = LinearRegression()

        # Train the model on the polynomial features training data
        model.fit(X_train_poly, y_train)

        # Use the trained model to predict the target values in the test set
        y_pred = model.predict(X_test_poly)

        # Calculate the Mean Squared Error of the model on the test set
        mse = mean_squared_error(y_test, y_pred)

        # Store the Mean Squared Error in the list
        mse_list.append(mse)

    # Plot the Mean Squared Error as a function of the degree of the polynomial for the current random state
    plt.plot(degrees, mse_list, marker='o', label=f'Random State {random_state}')

plt.title('Mean Squared Error vs. Degree of Polynomial for Different Random States')
plt.xlabel('Degree of Polynomial')
plt.ylabel('Mean Squared Error')
plt.legend()
plt.grid(True)
plt.show()

On Graph similar pattern find based on random state, but MSE is vary by changing random state. Idealy if variance is low all line will start overlap each other. There is huge difference between pink line and brown line where only random state change from 5 to 6.

K-кратная перекрестная проверка-

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

При K-кратной перекрестной проверке мы выбираем значение K, часто 5 или 10, которое доказало свою эффективность. Вот как это работает:

Разделение данных. Мы разделяем наши данные на две части: training и test data.

Выбор K: в обучающих данных мы выбираем value of K, обычно 5 или 10.. Это определяет, на сколько подмножеств или «складок» будут разделены наши данные.

Обучение модели. Теперь мы обучаем несколько моделей в зависимости от количества сгибов. Например, если K равен 5, мы создаем 5 моделей. Каждая модель обучается с использованием различной комбинации складок с использованием одного алгоритма машинного обучения, такого как «линейная регрессия».

Проверка и тестирование. Одна складка зарезервирована для проверки, а остальные используются для обучения. Этот процесс повторяется для каждой складки, поэтому каждая складка по очереди становится набором проверки.

Точность расчета. Каждая модель дает accuracy score. Мы находим средний показатель точности путем сложения индивидуальных оценок и деления на количество моделей.

Итоговая средняя оценка. Средняя оценка точности дает нам общую производительность нашей модели с использованием K-fold cross-validation.

Преимущества перекрестной проверки K-Fold -

  1. Уменьшение дисперсии:
  • В LOOCV оценка R2 во многом зависит от одной точки данных. Это может привести к высокой степени изменчивости результатов. С другой стороны, проверка K-Fold использует для тестирования всю кратность, охватывающую несколько точек данных. Это выгодно, поскольку означает, что >Оценка производительности менее чувствительна к конкретному случайному разделению данных. Проверка K-Fold способствует более стабильной и надежной оценке производительности модели.

2. Вычислительная экономичность:

  • Принимает less time и space по сравнению с LOOCV. Здесь обучалось только 5 или 10 моделей, но в LOOCV мы обучали модель в зависимости от количества строк.

Недостатки перекрестной проверки K-Fold –

  1. Возможность высокой предвзятости:
  • Если k слишком мало, может возникнуть высокая погрешность, если тестовый набор не будет репрезентативным для всей совокупности. K-fold нельзя использовать на модели, которая уже имеет высокую предвзятость, так как это ухудшит результат.

2. Может не работать с несбалансированным набором данных:

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

Когда использовать-

  • Когда у нас есть large dataset, когда данные evenly distributed.
Degree of Polynomial on K- Fold Cross Validation Technique using 10 Folds
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import KFold
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import mean_squared_error

# Load the Auto MPG dataset
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data"
column_names = ['MPG', 'Cylinders', 'Displacement', 'Horsepower', 'Weight', 'Acceleration', 'Model Year', 'Origin']
auto_mpg = pd.read_csv(url, names=column_names, delim_whitespace=True, na_values='?')

# Drop rows with missing values
auto_mpg = auto_mpg.dropna()

# Define features and target
features = auto_mpg[['Horsepower']]  # Only use 'Horsepower' as a feature
target = auto_mpg['MPG']

# Convert to numpy arrays for easier manipulation
X = features.to_numpy()
y = target.to_numpy()

# List of polynomial degrees
degrees = list(range(1, 11))  # Extend degrees to 10

# Create a plot
plt.figure(figsize=(10, 6))

# Create a 10-fold cross validator
kf = KFold(n_splits=10, shuffle=True, random_state=1)

for i, (train_index, test_index) in enumerate(kf.split(X)):
    # Split the data
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]

    # List to store Mean Squared Errors
    mse_list = []

    for degree in degrees:
        # Add polynomial features
        poly = PolynomialFeatures(degree=degree)
        X_train_poly = poly.fit_transform(X_train)
        X_test_poly = poly.transform(X_test)

        # Create a linear regression model
        model = LinearRegression()

        # Train the model on the polynomial features training data
        model.fit(X_train_poly, y_train)

        # Use the trained model to predict the target values in the test set
        y_pred = model.predict(X_test_poly)

        # Calculate the Mean Squared Error of the model on the test set
        mse = mean_squared_error(y_test, y_pred)

        # Store the Mean Squared Error in the list
        mse_list.append(mse)

    # Plot the Mean Squared Error as a function of the degree of the polynomial for the current fold
    plt.plot(degrees, mse_list, marker='o', label=f'Fold {i + 1}')

plt.title('Mean Squared Error vs. Degree of Polynomial for Different Folds')
plt.xlabel('Degree of Polynomial')
plt.ylabel('Mean Squared Error')
plt.legend()
plt.grid(True)
plt.show()

On Graph- we have 10 folds, excpept orange line all line comes nearby 0.4 to 0.7 which shows reduction in variance

Стратифицированный K-фолд CV-

Это вариант техники перекрестной проверки K-Fold.

Мы можем использовать Stratified, когда у нас есть «Imbalanced Dataset”».

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

  • Представьте себе набор данных с тремя классами: “yes,” “no,” and “maybe,” и мы measuring their widths используем a ratio. Когда мы реализуем stratified cross-validation, мы создаем складки, сохраняющие proportions of all three classes consistently. Этот метод гарантирует, что каждая складка имеет представление каждого класса, устраняя проблему завышенной точности и обеспечивая надежные результаты.
  • Стратегия стратифицированной перекрестной проверки проста, но эффективна. Он сохраняет исходное распределение классов по сгибам, сохраняя те же соотношения. Например, если 95 % данных относятся к классу «да», а 5 % — к классу «нет». , это соотношение остается постоянным для всех сгибов.
  • Приняв этот подход, мы overcome the problem of having a disproportionately high accuracy due to an unequal distribution of classes.
from sklearn.datasets import load_iris
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression

# Load iris dataset
data = load_iris()
X, y = data.data, data.target

# Create a Logistic Regression model
model = LogisticRegression(max_iter=10000, random_state=42)

# Create StratifiedKFold object
skf = StratifiedKFold(n_splits=5, random_state=42, shuffle=True)

# Perform stratified cross validation
scores = cross_val_score(model, X, y, cv=skf, scoring='accuracy')

# Print the accuracy for each fold
print("Accuracies for each fold: ", scores)
print("Mean accuracy across all folds: ", scores.mean())

Найди меня здесь:

Github: https://github.com/21Nimisha

Linkedin: https://www.linkedin.com/in/nimisha-singh-b6183419/

Сообщение от AI Mind

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь: