# neurologic

Dead-simple AI training. Build and train neural networks in 3 lines of Python.

```python
from neurologic import Model, Trainer

model = Model.classifier(784, [128, 64], 10)
trainer = Trainer(model)
trainer.fit(X_train, y_train, epochs=10)
```

## Install

```bash
pip install neurologic
```

Or install from source:

```bash
git clone <your-repo-url>
cd neurologic
pip install -e .
```

## Quick Start

### Tabular / CSV Data

```python
from neurologic import Model, Trainer, load_csv

# Load your data
X, y = load_csv("data.csv", target="label")

# Build a model: 4 features → [64, 32] hidden layers → 3 classes
model = Model.classifier(4, [64, 32], 3)
trainer = Trainer(model)

# Train
trainer.fit(X, y, epochs=20)

# Check results
print(trainer.evaluate(X, y))  # {'loss': 0.45, 'accuracy': 0.92}

# Predict
predictions = trainer.predict(X[:5])

# Save
trainer.save("my_model.pt")

# Load later
trainer = Trainer.load("my_model.pt")
```

### Image Classification

Organize images in folders by class:

```
my_images/
    cats/
        cat1.jpg
        cat2.jpg
    dogs/
        dog1.jpg
        dog2.jpg
```

```python
from neurologic import Model, Trainer, load_images

images, labels = load_images("my_images/", image_size=224)

# Pretrained ResNet — already smart, just fine-tune for your classes
model = Model.vision(num_classes=2)
trainer = Trainer(model, lr=0.0001)

trainer.fit(images, labels, epochs=5)
trainer.save("cat_vs_dog.pt")
```

### Text Classification

```python
from neurologic import Model, Trainer, load_text

X, y = load_text("reviews.csv", text_col="text", label_col="sentiment", vocab_size=5000)

model = Model.text(vocab_size=5000, num_classes=2)
trainer = Trainer(model)

trainer.fit(X, y, epochs=10)
print(trainer.evaluate(X, y))
```

## Dynamic Learning Rate

The learning rate controls how fast the model learns. Too high = unstable, too low = slow. Dynamic schedulers adjust it automatically during training.

```python
# Plateau — drops LR by half when loss stops improving (recommended)
trainer = Trainer(model, lr=0.001, scheduler="plateau")

# Cosine — smoothly decays LR to zero over training
trainer = Trainer(model, lr=0.001, scheduler="cosine")

# Warmup + Cosine — ramps up LR for the first 10% of epochs, then cosine decay
# Good for large models that are sensitive to early training
trainer = Trainer(model, lr=0.001, scheduler="warmup_cosine")

# Step — drops LR by 10x every third of training
trainer = Trainer(model, lr=0.01, scheduler="step")
```

The current LR is printed each epoch and tracked in the history:

```python
history = trainer.fit(X, y, epochs=30)
print(history["lr"])  # [0.001, 0.001, 0.001, 0.0005, ...]
```

## Layer Growth

Start with a small model and let it grow deeper automatically when it stops improving. The trainer adds new hidden layers during training — existing weights are preserved.

```python
model = Model.classifier(10, [32], 3)  # start small: 1 hidden layer
trainer = Trainer(model, scheduler="plateau")

# grow_epochs=10 → check every 10 epochs, add a layer if loss plateaued
# grow_max=3    → add at most 3 layers
trainer.fit(X, y, epochs=50, grow_epochs=10, grow_max=3)
```

Output:

```
Training 'neurologic-classifier' on mps — 900 train samples
  Epoch 10/50  train_loss=0.8213  val_loss=0.8150
  >> Grew layer #1 (size=32), model now: neurologic-classifier (2 hidden layers, 2,211 params)
  Epoch 20/50  train_loss=0.6841  val_loss=0.7012
  >> Grew layer #2 (size=32), model now: neurologic-classifier (3 hidden layers, 3,299 params)
  Epoch 30/50  train_loss=0.5102  val_loss=0.5530
Done. Final train_loss=0.4231
```

You can also grow manually:

```python
model = Model.classifier(10, [64], 3)
model.grow()       # adds a layer matching previous width (64)
model.grow(128)    # adds a 128-wide layer
print(model)       # neurologic-classifier (3 hidden layers, 14,531 params)
```

## All Options

### Model.classifier()

```python
model = Model.classifier(
    input_size,              # number of input features (required)
    hidden,                  # list of hidden layer sizes (required)
    output_size,             # number of classes, or 1 for regression (required)
    activation="relu",       # "relu", "gelu", "tanh", "sigmoid"
    dropout=0.0,             # dropout rate between layers (0-1)
    batch_norm=False,        # batch normalization between layers
    name="my-model",         # label for this model
)
```

### Model.vision()

```python
model = Model.vision(
    num_classes,             # number of output classes (required)
    backbone="resnet18",     # "resnet18", "resnet50", "mobilenet_v2", etc.
    pretrained=True,         # start from ImageNet weights
    freeze_backbone=False,   # only train the final layer
    dropout=0.0,             # dropout before final layer
    name="my-vision-model",
)
```

### Model.text()

```python
model = Model.text(
    vocab_size,              # vocabulary size (required)
    num_classes,             # number of output classes (required)
    embed_dim=128,           # word embedding size
    hidden_dim=256,          # LSTM hidden size
    num_layers=1,            # stacked LSTM layers
    dropout=0.0,             # regularization
    name="my-text-model",
)
```

### Trainer()

```python
trainer = Trainer(
    model,                   # any nn.Module (required)
    lr=0.001,                # learning rate
    optimizer="adam",         # "adam", "adamw", "sgd"
    loss=None,               # custom loss fn (auto-picks CrossEntropy or MSE)
    device=None,             # "cpu", "cuda", "mps" (auto-detects)
    scheduler=None,          # "plateau", "cosine", "warmup_cosine", "step"
    name=None,               # label for this training run
)
```

### trainer.fit()

```python
history = trainer.fit(
    X, y,                    # input and target tensors (required)
    epochs=10,               # number of training epochs
    batch_size=32,           # mini-batch size
    validation_split=0.1,    # fraction held out for validation
    early_stopping=0,        # stop after N epochs with no improvement
    grow_epochs=0,           # add a layer every N epochs if plateaued
    grow_size=None,          # width of grown layers (None = match previous)
    grow_max=5,              # max layers to grow
)
```

### Other Methods

```python
trainer.summary()                   # print model config
trainer.evaluate(X, y)              # returns {"loss": ..., "accuracy": ...}
trainer.predict(X)                  # returns prediction tensor
trainer.save("model.pt")            # save to disk
trainer = Trainer.load("model.pt")  # load from disk
trainer.lr                          # current learning rate
```

## Data Loaders

```python
from neurologic import load_csv, load_images, load_text

# CSV → tensors
X, y = load_csv("data.csv", target="label")
X, y = load_csv("data.csv", target="label", features=["col1", "col2"])

# Image folders → tensors
images, labels = load_images("dataset/", image_size=224)

# Text CSV → tokenized tensors
X, y = load_text("reviews.csv", text_col="text", label_col="sentiment",
                  vocab_size=10000, max_len=256)
```

## Tips

- **Start simple.** `Model.classifier(in, [64, 32], out)` is enough for most tabular problems.
- **Use `scheduler="plateau"`** for hands-off LR tuning. It just works.
- **Use `freeze_backbone=True`** for vision if you have < 1000 images. Fine-tunes faster.
- **Use `early_stopping=5`** to avoid overfitting. Stops when validation loss flatlines.
- **Use `grow_epochs`** when you're not sure how big the model should be. Let it figure it out.
- **GPU is automatic.** CUDA, Apple Silicon (MPS), or CPU — no config needed.

## Requirements

- Python 3.10+
- PyTorch 2.0+
- torchvision, pandas, pillow, tqdm
