446 lines
13 KiB
C
446 lines
13 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
#include "eval.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
#include <time.h>
|
|
|
|
/* ============================================================= */
|
|
/* Task */
|
|
/* ============================================================= */
|
|
|
|
fe_task* fe_task_new(const char* name, fe_task_type type, fd_dataset* dataset) {
|
|
if (!name) return NULL;
|
|
|
|
fe_task* task = calloc(1, sizeof(fe_task));
|
|
if (!task) return NULL;
|
|
|
|
task->name = strdup(name);
|
|
task->type = type;
|
|
task->dataset = dataset; /* Does not take ownership */
|
|
task->input_field = strdup("text");
|
|
task->target_field = strdup("label");
|
|
task->num_fewshot = 0;
|
|
|
|
return task;
|
|
}
|
|
|
|
void fe_task_free(fe_task* task) {
|
|
if (!task) return;
|
|
|
|
free(task->name);
|
|
free(task->input_field);
|
|
free(task->target_field);
|
|
free(task);
|
|
}
|
|
|
|
void fe_task_set_fields(fe_task* task, const char* input_field, const char* target_field) {
|
|
if (!task) return;
|
|
|
|
if (input_field) {
|
|
free(task->input_field);
|
|
task->input_field = strdup(input_field);
|
|
}
|
|
if (target_field) {
|
|
free(task->target_field);
|
|
task->target_field = strdup(target_field);
|
|
}
|
|
}
|
|
|
|
void fe_task_set_fewshot(fe_task* task, int num_fewshot) {
|
|
if (task) {
|
|
task->num_fewshot = num_fewshot >= 0 ? num_fewshot : 0;
|
|
}
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Result */
|
|
/* ============================================================= */
|
|
|
|
static fe_result* result_new(const char* task_name, fe_task_type type) {
|
|
fe_result* result = calloc(1, sizeof(fe_result));
|
|
if (!result) return NULL;
|
|
|
|
result->task_name = task_name ? strdup(task_name) : NULL;
|
|
result->type = type;
|
|
|
|
return result;
|
|
}
|
|
|
|
void fe_result_free(fe_result* result) {
|
|
if (!result) return;
|
|
|
|
free(result->task_name);
|
|
free(result->subscores);
|
|
free(result);
|
|
}
|
|
|
|
void fe_results_free(fe_result** results, int num_results) {
|
|
if (!results) return;
|
|
|
|
for (int i = 0; i < num_results; i++) {
|
|
fe_result_free(results[i]);
|
|
}
|
|
free(results);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Metrics */
|
|
/* ============================================================= */
|
|
|
|
double fe_perplexity(float* logprobs, int len) {
|
|
if (!logprobs || len <= 0) return 0.0;
|
|
|
|
double sum = 0.0;
|
|
for (int i = 0; i < len; i++) {
|
|
sum += logprobs[i];
|
|
}
|
|
|
|
double avg_logprob = sum / len;
|
|
return exp(-avg_logprob);
|
|
}
|
|
|
|
double fe_accuracy(int* predictions, int* labels, int n) {
|
|
if (!predictions || !labels || n <= 0) return 0.0;
|
|
|
|
int correct = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
if (predictions[i] == labels[i]) {
|
|
correct++;
|
|
}
|
|
}
|
|
|
|
return (double)correct / n;
|
|
}
|
|
|
|
double fe_accuracy_topk(float** logits, int* labels, int n, int vocab_size, int k) {
|
|
if (!logits || !labels || n <= 0 || k <= 0) return 0.0;
|
|
|
|
int correct = 0;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
/* Find top-k predictions */
|
|
int* top_k = calloc(k, sizeof(int));
|
|
float* top_vals = calloc(k, sizeof(float));
|
|
|
|
for (int j = 0; j < k; j++) {
|
|
top_vals[j] = -INFINITY;
|
|
}
|
|
|
|
for (int v = 0; v < vocab_size; v++) {
|
|
float val = logits[i][v];
|
|
/* Insert into top-k if larger than smallest */
|
|
if (val > top_vals[k - 1]) {
|
|
int pos = k - 1;
|
|
while (pos > 0 && val > top_vals[pos - 1]) {
|
|
top_vals[pos] = top_vals[pos - 1];
|
|
top_k[pos] = top_k[pos - 1];
|
|
pos--;
|
|
}
|
|
top_vals[pos] = val;
|
|
top_k[pos] = v;
|
|
}
|
|
}
|
|
|
|
/* Check if label is in top-k */
|
|
for (int j = 0; j < k; j++) {
|
|
if (top_k[j] == labels[i]) {
|
|
correct++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
free(top_k);
|
|
free(top_vals);
|
|
}
|
|
|
|
return (double)correct / n;
|
|
}
|
|
|
|
double fe_f1_score(int* predictions, int* labels, int n) {
|
|
if (!predictions || !labels || n <= 0) return 0.0;
|
|
|
|
/* Binary F1 score */
|
|
int tp = 0, fp = 0, fn = 0;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
if (predictions[i] == 1 && labels[i] == 1) tp++;
|
|
else if (predictions[i] == 1 && labels[i] == 0) fp++;
|
|
else if (predictions[i] == 0 && labels[i] == 1) fn++;
|
|
}
|
|
|
|
if (tp == 0) return 0.0;
|
|
|
|
double precision = (double)tp / (tp + fp);
|
|
double recall = (double)tp / (tp + fn);
|
|
|
|
return 2.0 * precision * recall / (precision + recall);
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Evaluator */
|
|
/* ============================================================= */
|
|
|
|
struct fe_evaluator {
|
|
ft_tokenizer* tokenizer;
|
|
fe_task** tasks;
|
|
int num_tasks;
|
|
int capacity;
|
|
};
|
|
|
|
fe_evaluator* fe_new(ft_tokenizer* tokenizer) {
|
|
fe_evaluator* eval = calloc(1, sizeof(fe_evaluator));
|
|
if (!eval) return NULL;
|
|
|
|
eval->tokenizer = tokenizer;
|
|
eval->capacity = 8;
|
|
eval->tasks = calloc(eval->capacity, sizeof(fe_task*));
|
|
|
|
return eval;
|
|
}
|
|
|
|
void fe_free(fe_evaluator* eval) {
|
|
if (!eval) return;
|
|
|
|
/* Note: doesn't free tasks, just the pointers */
|
|
free(eval->tasks);
|
|
free(eval);
|
|
}
|
|
|
|
void fe_add_task(fe_evaluator* eval, fe_task* task) {
|
|
if (!eval || !task) return;
|
|
|
|
if (eval->num_tasks >= eval->capacity) {
|
|
eval->capacity *= 2;
|
|
eval->tasks = realloc(eval->tasks, eval->capacity * sizeof(fe_task*));
|
|
}
|
|
|
|
eval->tasks[eval->num_tasks++] = task;
|
|
}
|
|
|
|
fe_result* fe_run_task(fe_evaluator* eval, fe_task* task,
|
|
fe_model_fn model_fn, void* model_ctx) {
|
|
if (!eval || !task || !model_fn) return NULL;
|
|
if (!task->dataset || fd_len(task->dataset) == 0) return NULL;
|
|
|
|
fe_result* result = result_new(task->name, task->type);
|
|
if (!result) return NULL;
|
|
|
|
clock_t start = clock();
|
|
|
|
int n = fd_len(task->dataset);
|
|
result->num_samples = n;
|
|
|
|
switch (task->type) {
|
|
case FE_TASK_PERPLEXITY: {
|
|
double total_logprob = 0.0;
|
|
int total_tokens = 0;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
fd_sample* sample = fd_get(task->dataset, i);
|
|
const char* text = fd_sample_get(sample, task->input_field);
|
|
if (!text) continue;
|
|
|
|
/* Tokenize */
|
|
int len;
|
|
int* ids = ft_encode(eval->tokenizer, text, &len);
|
|
if (!ids || len == 0) {
|
|
free(ids);
|
|
continue;
|
|
}
|
|
|
|
/* Get model output (logits) */
|
|
int output_len;
|
|
float* logits = model_fn(ids, len, &output_len, model_ctx);
|
|
|
|
if (logits && output_len > 0) {
|
|
/* Sum log probabilities (simplified) */
|
|
for (int t = 0; t < output_len; t++) {
|
|
/* Assume logits[t] is log prob of correct token */
|
|
total_logprob += logits[t];
|
|
}
|
|
total_tokens += output_len;
|
|
free(logits);
|
|
}
|
|
|
|
free(ids);
|
|
}
|
|
|
|
if (total_tokens > 0) {
|
|
result->score = exp(-total_logprob / total_tokens);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case FE_TASK_ACCURACY: {
|
|
int correct = 0;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
fd_sample* sample = fd_get(task->dataset, i);
|
|
const char* text = fd_sample_get(sample, task->input_field);
|
|
const char* label_str = fd_sample_get(sample, task->target_field);
|
|
if (!text || !label_str) continue;
|
|
|
|
int len;
|
|
int* ids = ft_encode(eval->tokenizer, text, &len);
|
|
if (!ids) continue;
|
|
|
|
int output_len;
|
|
float* output = model_fn(ids, len, &output_len, model_ctx);
|
|
|
|
if (output && output_len > 0) {
|
|
/* Find predicted class (argmax) */
|
|
int pred = 0;
|
|
float max_val = output[0];
|
|
for (int j = 1; j < output_len; j++) {
|
|
if (output[j] > max_val) {
|
|
max_val = output[j];
|
|
pred = j;
|
|
}
|
|
}
|
|
|
|
int label = atoi(label_str);
|
|
if (pred == label) correct++;
|
|
|
|
free(output);
|
|
}
|
|
|
|
free(ids);
|
|
}
|
|
|
|
result->score = (double)correct / n;
|
|
break;
|
|
}
|
|
|
|
case FE_TASK_MULTIPLE_CHOICE:
|
|
case FE_TASK_GENERATION:
|
|
/* Simplified: just return placeholder */
|
|
result->score = 0.0;
|
|
break;
|
|
}
|
|
|
|
clock_t end = clock();
|
|
result->elapsed_seconds = (double)(end - start) / CLOCKS_PER_SEC;
|
|
|
|
return result;
|
|
}
|
|
|
|
fe_result** fe_run(fe_evaluator* eval, fe_model_fn model_fn, void* model_ctx) {
|
|
if (!eval || !model_fn || eval->num_tasks == 0) return NULL;
|
|
|
|
fe_result** results = calloc(eval->num_tasks, sizeof(fe_result*));
|
|
if (!results) return NULL;
|
|
|
|
for (int i = 0; i < eval->num_tasks; i++) {
|
|
results[i] = fe_run_task(eval, eval->tasks[i], model_fn, model_ctx);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Built-in Task Loaders */
|
|
/* ============================================================= */
|
|
|
|
fe_task* fe_load_hellaswag(const char* path) {
|
|
fd_dataset* ds = fd_load_jsonl(path);
|
|
if (!ds) return NULL;
|
|
|
|
fe_task* task = fe_task_new("hellaswag", FE_TASK_MULTIPLE_CHOICE, ds);
|
|
if (task) {
|
|
fe_task_set_fields(task, "ctx", "label");
|
|
}
|
|
|
|
return task;
|
|
}
|
|
|
|
fe_task* fe_load_classification(const char* path, const char* input_field,
|
|
const char* label_field) {
|
|
fd_dataset* ds = fd_load_jsonl(path);
|
|
if (!ds) return NULL;
|
|
|
|
fe_task* task = fe_task_new("classification", FE_TASK_ACCURACY, ds);
|
|
if (task) {
|
|
fe_task_set_fields(task, input_field, label_field);
|
|
}
|
|
|
|
return task;
|
|
}
|
|
|
|
fe_task* fe_load_perplexity(const char* path, const char* text_field) {
|
|
fd_dataset* ds = fd_load_jsonl(path);
|
|
if (!ds) return NULL;
|
|
|
|
fe_task* task = fe_task_new("perplexity", FE_TASK_PERPLEXITY, ds);
|
|
if (task) {
|
|
fe_task_set_fields(task, text_field, NULL);
|
|
}
|
|
|
|
return task;
|
|
}
|
|
|
|
/* ============================================================= */
|
|
/* Utility */
|
|
/* ============================================================= */
|
|
|
|
void fe_print_result(fe_result* result) {
|
|
if (!result) {
|
|
printf("(null result)\n");
|
|
return;
|
|
}
|
|
|
|
const char* type_name = "unknown";
|
|
switch (result->type) {
|
|
case FE_TASK_PERPLEXITY: type_name = "perplexity"; break;
|
|
case FE_TASK_ACCURACY: type_name = "accuracy"; break;
|
|
case FE_TASK_MULTIPLE_CHOICE: type_name = "multiple_choice"; break;
|
|
case FE_TASK_GENERATION: type_name = "generation"; break;
|
|
}
|
|
|
|
printf("%s (%s): %.4f", result->task_name ? result->task_name : "unnamed",
|
|
type_name, result->score);
|
|
|
|
if (result->num_samples > 0) {
|
|
printf(" (%d samples, %.2fs)", result->num_samples, result->elapsed_seconds);
|
|
}
|
|
printf("\n");
|
|
}
|
|
|
|
void fe_print_results(fe_result** results, int num_results) {
|
|
if (!results) return;
|
|
|
|
printf("\n=== Evaluation Results ===\n");
|
|
for (int i = 0; i < num_results; i++) {
|
|
fe_print_result(results[i]);
|
|
}
|
|
printf("==========================\n");
|
|
}
|
|
|
|
bool fe_export_json(fe_result** results, int num_results, const char* path) {
|
|
if (!results || !path) return false;
|
|
|
|
FILE* f = fopen(path, "w");
|
|
if (!f) return false;
|
|
|
|
fprintf(f, "{\n \"results\": [\n");
|
|
|
|
for (int i = 0; i < num_results; i++) {
|
|
fe_result* r = results[i];
|
|
if (!r) continue;
|
|
|
|
fprintf(f, " {\n");
|
|
fprintf(f, " \"task\": \"%s\",\n", r->task_name ? r->task_name : "");
|
|
fprintf(f, " \"score\": %.6f,\n", r->score);
|
|
fprintf(f, " \"num_samples\": %d,\n", r->num_samples);
|
|
fprintf(f, " \"elapsed_seconds\": %.2f\n", r->elapsed_seconds);
|
|
fprintf(f, " }%s\n", (i < num_results - 1) ? "," : "");
|
|
}
|
|
|
|
fprintf(f, " ]\n}\n");
|
|
fclose(f);
|
|
|
|
return true;
|
|
}
|