Question
## Scenario
You are given a matrix of labeled training examples and a batch of query examples. Implement squared-Euclidean 1-nearest-neighbor classification, then show that the same nearest-neighbor choice can be expressed as the output of an affine layer.
For a query vector `x` and training vector `x_i`:
`-||x - x_i||² = 2x·x_i - ||x_i||² - ||x||²`
The final term is constant across all training examples for a fixed query, so it does not affect the winning index. This yields an affine score matrix with `W = 2 * X_train.T` and `b_i = -||x_i||²`.
## Requirements
Implement both functions below:
```python
def one_nn_predict(
X_train: np.ndarray,
y_train: np.ndarray,
X_query: np.ndarray,
) -> np.ndarray: ...
def one_nn_network_forward(
X_train: np.ndarray,
X_query: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]: ...
```
- `X_train` has shape `(n_train, d)` and `X_query` has shape `(n_query, d)`.
- `y_train` has one label per training row.
- `one_nn_predict` returns one label per query.
- `one_nn_network_forward` returns `(probabilities, nearest_indices)`, where probabilities are the row-wise softmax of the affine scores.
- Break exact distance ties in favor of the lowest training index.
- Use vectorized NumPy operations; avoid a nested Python loop over queries and training rows.
- Do not mutate the inputs.
Assume all arrays are non-empty, dimensions match, and numeric inputs contain finite values.
## Edge cases to discuss
- Duplicate training vectors with different labels.
- Very large values and numerically stable softmax.
- The memory cost of materializing the full query-by-training score matrix.
## Follow-ups
1. Project labels through a second matrix so the entire prediction can be described as a small network.
2. Process queries or training examples in chunks when the score matrix is too large.
3. Replace squared Euclidean distance with Manhattan distance. Explain why a single affine layer no longer gives the same general scoring rule.