Make an advanced Q-Learning NNUE search python script based-off the script below, (with 12K parameters).

```
import requests
import chess
import chess.pgn
import torch
import torch.nn as nn
import torch.optim as optim
import io
from stockfish import Stockfish

class NNUE(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(NNUE, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.fc2 = nn.Linear(hidden_size, hidden_size)
        self.fc3 = nn.Linear(hidden_size, output_size)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.relu(self.fc1(x))
        x = self.relu(self.fc2(x))
        x = self.fc3(x)
        return x

def download_lichess_games(username, max_games=400):
    url = f"https://lichess.org/api/games/user/{username}?max={max_games}&pgnInJson=true"
    response = requests.get(url)
    if response.status_code != 200:
        raise Exception(f"Failed to fetch data from Lichess API: {response.status_code}")
    try:
        games = response.json()
    except ValueError:
        raise Exception(f"Failed to parse JSON response from Lichess API: {response.text}")
    return games

def extract_fen_positions_and_evaluations(games, stockfish_path, depth=15):
    stockfish = Stockfish(stockfish_path)
    positions = []
    evaluations = []
    for i, game in enumerate(games):
        pgn = game.get('pgn')
        if not pgn:
            print(f"No PGN found for game {i + 1}.")
            continue
        
        try:
            pgn_io = io.StringIO(pgn)
            game = chess.pgn.read_game(pgn_io)
            if game is None:
                print(f"Failed to parse PGN for game {i + 1}.")
                continue
            
            board = game.board()
            for move in game.mainline_moves():
                board.push(move)
                positions.append(board.fen())
                stockfish.set_fen_position(board.fen())
                evaluation = stockfish.get_evaluation()
                if evaluation['type'] == 'cp':
                    evaluations.append(evaluation['value'] / 100.0)  
                elif evaluation['type'] == 'mate':
                    evaluations.append(1000 if evaluation['value'] > 0 else -1000)  
        except Exception as e:
            print(f"Error processing PGN for game {i + 1}: {e}")
            continue
    return positions, evaluations

def board_to_tensor(board):
    pieces = ['p', 'n', 'b', 'r', 'q', 'k', 'P', 'N', 'B', 'R', 'Q', 'K']
    board_tensor = torch.zeros(64 * 12)
    for square in chess.SQUARES:
        piece = board.piece_at(square)
        if piece:
            index = pieces.index(piece.symbol())
            board_tensor[square * 12 + index] = 1
    return board_tensor

def preprocess_data(positions, evaluations):
    data = []
    for position, evaluation in zip(positions, evaluations):
        board = chess.Board(position)
        input_tensor = board_to_tensor(board)
        target_tensor = torch.tensor([evaluation], dtype=torch.float32)
        data.append((input_tensor, target_tensor))
    return data

def train_model(data, input_size, hidden_size, output_size, epochs=10, learning_rate=0.001):
    if not data:
        raise ValueError("No data available for training.")
    
    model = NNUE(input_size, hidden_size, output_size)
    criterion = nn.MSELoss()
    optimizer = optim.Adam(model.parameters(), lr=learning_rate)

    for epoch in range(epochs):
        total_loss = 0
        for input_tensor, target_tensor in data:
            optimizer.zero_grad()
            output = model(input_tensor)
            loss = criterion(output, target_tensor)
            loss.backward()
            optimizer.step()
            total_loss += loss.item()
        average_loss = total_loss / len(data)
        print(f'Epoch {epoch+1}/{epochs}, Average Loss: {average_loss}')

    return model

# Download games from Lichess
username = "NoOneCanBeatMe4"
try:
    games = download_lichess_games(username)
    print(f"Downloaded {len(games)} games from Lichess.")
except Exception as e:
    print(e)
    games = []


stockfish_path = "stockfish-windows-x86-64-sse41-popcnt.exe"  
positions, evaluations = extract_fen_positions_and_evaluations(games, stockfish_path)
print(f"Extracted {len(positions)} positions and evaluations.")

data = preprocess_data(positions, evaluations)
print(f"Preprocessed {len(data)} data points.")
input_size = 64 * 12
hidden_size = 256
output_size = 1

if data:
    # Train the NNUE model
    trained_model = train_model(data, input_size, hidden_size, output_size)

    # Save the trained model
    torch.save(trained_model.state_dict(), 'nnue_model.pth')
    print("Saved trained model to nnue_model.pth.")
else:
    print("No data available for training.")