# your code goes here
import numpy as np

class XNORNeuralNetwork:
    def __init__(self, lr=0.5):
        np.random.seed(42)
        # Ключевой момент: 4 нейрона в скрытом слое для 4 примеров
        self.W1 = np.random.randn(2, 4) * 1.0
        self.b1 = np.random.randn(1, 4) * 0.5
        self.W2 = np.random.randn(4, 1) * 1.0
        self.b2 = np.random.randn(1, 1) * 0.5
        self.lr = lr
    
    def tanh(self, x):
        return np.tanh(x)
    
    def tanh_derivative(self, x):
        return 1 - np.tanh(x) ** 2
    
    def sigmoid(self, x):
        x = np.clip(x, -500, 500)
        return 1 / (1 + np.exp(-x))
    
    def sigmoid_derivative(self, x):
        return x * (1 - x)
    
    def forward(self, X):
        self.z1 = np.dot(X, self.W1) + self.b1
        self.a1 = self.tanh(self.z1)  # tanh вместо сигмоиды
        
        self.z2 = np.dot(self.a1, self.W2) + self.b2
        self.a2 = self.sigmoid(self.z2)
        
        return self.a2
    
    def train(self, X, y, epochs=20000, verbose=False):
        # Расширяем данные: добавляем инвертированные примеры
        X_augmented = np.vstack([X, 1 - X])
        y_augmented = np.vstack([y, 1 - y])
        
        # Добавляем небольшой шум для разрушения симметрии
        noise = np.random.randn(*X_augmented.shape) * 0.01
        X_augmented = X_augmented + noise
        X_augmented = np.clip(X_augmented, 0, 1)
        
        losses = []
        
        for epoch in range(epochs):
            # Перемешивание
            idx = np.random.permutation(len(X_augmented))
            X_batch = X_augmented[idx]
            y_batch = y_augmented[idx]
            
            # Прямой проход
            output = self.forward(X_batch)
            
            # Бинарная кросс-энтропия
            eps = 1e-8
            loss = -np.mean(y_batch * np.log(output + eps) + 
                          (1 - y_batch) * np.log(1 - output + eps))
            losses.append(loss)
            
            # Обратный проход
            m = X_batch.shape[0]
            
            # Выходной слой
            delta2 = (output - y_batch) / m
            
            # Скрытый слой
            delta1 = np.dot(delta2, self.W2.T) * self.tanh_derivative(self.z1)
            
            # Градиенты
            dW2 = np.dot(self.a1.T, delta2)
            db2 = np.sum(delta2, axis=0, keepdims=True)
            dW1 = np.dot(X_batch.T, delta1)
            db1 = np.sum(delta1, axis=0, keepdims=True)
            
            # L2 регуляризация
            lambda_l2 = 0.0001
            dW2 += lambda_l2 * self.W2
            dW1 += lambda_l2 * self.W1
            
            # Обновление с momentum
            if not hasattr(self, 'vW1'):
                self.vW1 = np.zeros_like(self.W1)
                self.vb1 = np.zeros_like(self.b1)
                self.vW2 = np.zeros_like(self.W2)
                self.vb2 = np.zeros_like(self.b2)
            
            momentum = 0.9
            self.vW2 = momentum * self.vW2 + self.lr * dW2
            self.vb2 = momentum * self.vb2 + self.lr * db2
            self.vW1 = momentum * self.vW1 + self.lr * dW1
            self.vb1 = momentum * self.vb1 + self.lr * db1
            
            self.W2 -= self.vW2
            self.b2 -= self.vb2
            self.W1 -= self.vW1
            self.b1 -= self.vb1
            
            # Адаптивное уменьшение learning rate
            if epoch % 5000 == 0 and epoch > 0:
                self.lr *= 0.7
            
            if verbose and epoch % 5000 == 0:
                pred = self.forward(X)
                acc = np.mean((pred > 0.5) == y)
                print(f"Epoch {epoch}, Loss: {loss:.6f}, Accuracy: {acc:.2f}, LR: {self.lr:.4f}")
        
        return losses


# ===== ДАННЫЕ =====
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=np.float64)
y = np.array([[1], [0], [0], [1]], dtype=np.float64)

print("=" * 70)
print("ГАРАНТИРОВАННОЕ ОБУЧЕНИЕ XNOR")
print("=" * 70)

# Множественные попытки с разными сидами
best_model = None
best_accuracy = 0
best_predictions = None

for attempt in range(10):
    np.random.seed(attempt)
    
    model = XNORNeuralNetwork(lr=0.1)
    losses = model.train(X, y, epochs=2000, verbose=False)
    
    predictions = model.forward(X)
    accuracy = np.mean((predictions > 0.5) == y)
    
    print(f"Попытка {attempt + 1}: точность = {accuracy*100:.0f}%")
    
    if accuracy > best_accuracy:
        best_accuracy = accuracy
        best_model = model
        best_predictions = predictions.copy()
    
    if accuracy == 1.0:
        print("✓ Достигнута 100% точность!")
        break

model = best_model
print(f"\nЛучшая точность: {best_accuracy*100:.0f}%")

# ===== ТЕСТИРОВАНИЕ С ИНВЕРСИЕЙ =====
print("\n" + "=" * 70)
print("ТЕСТИРОВАНИЕ С ИСПОЛЬЗОВАНИЕМ ИНВЕРСИИ")
print("=" * 70)

print(f"\n{'Вход':<12} {'Цель':<8} {'f(x)':<12} {'Предск.':<10} "
      f"{'f(~x)':<12} {'1-f(~x)':<12} {'Ансамбль':<12} {'Сумма':<10}")
print("-" * 90)

correct_direct = 0
correct_inv = 0
correct_ensemble = 0

for i in range(len(X)):
    # Прямой проход
    f_x = model.forward(X[i:i+1])[0][0]
    pred_direct = 1 if f_x > 0.5 else 0
    
    # Инвертированный проход
    X_inv = 1 - X[i:i+1]
    f_not_x = model.forward(X_inv)[0][0]
    
    # Предсказание через инверсию
    pred_from_inv = 1 if (1 - f_not_x) > 0.5 else 0
    
    # Ансамбль
    ensemble = (f_x + (1 - f_not_x)) / 2
    pred_ensemble = 1 if ensemble > 0.5 else 0
    
    # Сумма для проверки
    sum_check = f_x + f_not_x
    
    # Подсчёт точности
    target = int(y[i][0])
    if pred_direct == target: correct_direct += 1
    if pred_from_inv == target: correct_inv += 1
    if pred_ensemble == target: correct_ensemble += 1
    
    print(f"{str(X[i]):<12} {target:<8} {f_x:<12.6f} {pred_direct:<10} "
          f"{f_not_x:<12.6f} {1-f_not_x:<12.6f} {ensemble:<12.6f} {sum_check:<10.6f}")

print(f"\n{'='*70}")
print(f"РЕЗУЛЬТАТЫ:")
print(f"  Точность прямых предсказаний: {correct_direct}/4")
print(f"  Точность через инверсию:      {correct_inv}/4")
print(f"  Точность ансамбля:            {correct_ensemble}/4")

# ===== ПРОВЕРКА СВОЙСТВА ИНВЕРСИИ =====
print(f"\n{'='*70}")
print("ПРОВЕРКА СВОЙСТВА f(x) + f(~x) ≈ 1")
print(f"{'='*70}")

v00 = model.forward(np.array([[0, 0]]))[0][0]
v11 = model.forward(np.array([[1, 1]]))[0][0]
print(f"f(0,0) + f(1,1) = {v00:.6f} + {v11:.6f} = {v00+v11:.6f}")

v01 = model.forward(np.array([[0, 1]]))[0][0]
v10 = model.forward(np.array([[1, 0]]))[0][0]
print(f"f(0,1) + f(1,0) = {v01:.6f} + {v10:.6f} = {v01+v10:.6f}")

# ===== ДЕМОНСТРАЦИЯ ПРЕИМУЩЕСТВА АНСАМБЛЯ =====
print(f"\n{'='*70}")
print("ПРЕИМУЩЕСТВО АНСАМБЛЯ ПРИ ШУМЕ")
print(f"{'='*70}")

print("\nТест с добавлением шума к входам:")
for i in range(len(X)):
    # Добавляем шум
    noise = np.random.uniform(-0.25, 0.25, 2)
    noisy_x = np.clip(X[i] + noise, 0, 1).reshape(1, -1)
    
    # Прямое предсказание
    f_noisy = model.forward(noisy_x)[0][0]
    pred_noisy = 1 if f_noisy > 0.5 else 0
    
    # Ансамбль с инверсией
    f_noisy_inv = model.forward(1 - noisy_x)[0][0]
    ensemble_noisy = (f_noisy + (1 - f_noisy_inv)) / 2
    pred_ens_noisy = 1 if ensemble_noisy > 0.5 else 0
    
    target = int(y[i][0])
    status_direct = "✓" if pred_noisy == target else "✗"
    status_ens = "✓" if pred_ens_noisy == target else "✗"
    
    print(f"Вход {X[i]} -> зашумлён [{noisy_x[0][0]:.2f}, {noisy_x[0][1]:.2f}]:")
    print(f"  Прямое: {f_noisy:.4f} -> {pred_noisy} {status_direct}")
    print(f"  Ансамбль: {ensemble_noisy:.4f} -> {pred_ens_noisy} {status_ens}")