Gemma 3 en un modelo “multimodal” abierto basado en Gemini 2.0 de Google, eso quiere decir que no solo funciona con texto sino con imágenes y videos. Viene en tamaños de 1B, 4B, 12B y 27B (billones de parámetros). Google nos ha proporcionado este modelo para que nosotros podamos usarlo y adaptarlo a nuestras necesidades.
En este artículo re-entrenaremos Gemma 3 en su versión de 12 Billones de parámetros, en su componente de texto, no de visión
Gemma 3 es ideal para re-entrenarlo porque soporta 140 idiomas (nos interesa el Español), porque tiene una ventana de contexto muy amplia (cantidad de datos de entrada que puede analizar) de 128 mil tokens y porque la licencia nos permite incluso cuantizarlo (reducirlo) para que pueda correrse en GPUs de 20GB de memoria (para nuestro caso específico).
Preparación del entorno base
En la consola de comandos de Mac OS se procede a instalar Homebrew, el administrador de paquetes desarrollado para Mac OS que nos permitirá instalar y desinstalar aplicaciones computando de forma automática sus dependencias:
% /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Al momento de ejecutar este comando se pide que se instale XCode Command Line Tools, para lo cual seguimos el procedimiento que se nos presentó en la pantalla. A continuación se instala Python:
% brew install python@3.11 git wget macmon
Descargamos el Framework MLX y sus componentes de apoyo para luego proceder a instalar los requerimientos y librerías necesarias para el entrenamiento:
% git clone https://github.com/ml-explore/mlx-examples.git
% cd mlx-examples/lora
% pip3.11 install -r requirements.txt
% pip3.11 install mlx-lm
% pip3.11 install tensorflow datasets torch
% pip3.11 install huggingface_hub hf_transfer
Descargamos llama.cpp de Meta, que nos servirá mas adelante para cuantizar el modelo (volverlo mas pequeño) y convertirlo en formato GGUF (el formato estandard para un LLM) y así poderlo correr en Ollama:
% git clone https://github.com/ggerganov/llama.cpp
% pip3.11 install -r llama.cpp/requirements.txt
% cd llama.cpp
% cmake -B build
% cmake --build build --config Release
Para poder ejecutar cómodamente los comandos necesarios de aquí en adelante agregue al PATH el directorio donde quedaron los ejecutables del Framework MLX:
% echo "$HOME/Library/Python/3.11/bin/" >> /etc/paths
Registro en Huggingface
Huggingface es el gran repositorio de modelos LLM abiertos donde las personas y empresas que se registran pueden descargar los modelos de lenguaje que se publican allí.
Para que esta descarga automática sea posible debemos primero registrarnos en Huggingface. Una vez creada una cuenta, vamos al perfil del usuario, luego entramos a Settings y creamos un Access Token con todos los permisos para acceder a los repositorios. Este Access Token lo copiamos en un lugar seguro y luego en la consola de Mac OS creamos una variable de entorno para que automáticamente MLX la use y pueda conectarse a las API de Huggingface:
% export HF_TOKEN="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"
Aunque los LLM publicados en Huggingface son libres de acceso y la mayoría de ellos tienen una licencia que permite usarlos comercialmente, se debe solicitar autorización previa para el acceso a cada modelo y esperar a que el desarrollador del mismo lo autorice.
Se debe solicitar el acceso al modelo de Google Gemma 3 de 12 billones de parámetros en su versión “Instruction Trained” y lo hicimos visitando directamente el link y solicitando su acceso. Este proceso de autorización puede durar un par de días o incluso puede no requerirse según la licencia.
Descarga del modelo
Una vez que el acceso al modelo sea autorizado en Huggingface, procedemos a descargarlo y a convertirlo en el formato que el Framework de MLX entiende:
% mlx_lm.convert --hf-path google/gemma-3-12b-it --dtype bfloat16
% ls ~/.cache/huggingface/hub/
Automáticamente se conectaría a Huggingface mediante su API usando como autorización el Access Token que se creó en le punto anterior. Este proceso lo que hace es descargar el modelo original a la carpeta oculta cache del usuario de Mac OS y adicionalmente crea una carpeta llamada mlx_model donde se guarda el modelo convertido al formato que entiende el Framework MLX.
Dataset de entrenamiento

Se debe crear un Excel (para este caso de ejemplo) que contenga dos columnas y que la hoja de trabajo se llame dataset. La primera columna debería contener el input y la segunda el output. Aquí todavía no estamos hablando del prompt, sino de los datos que usaremos de entrada y de salida para entrenar el modelo. Piense en el input como aquel dato que usted quiere que el LLM analice y piense en el output como aquella respuesta que usted quiere que el LLM dé. En el input y en el output puede existir un nuevo conocimiento y un estilo de respuesta particular.
Un buen entrenamiento (fine tuning) está directamente relacionado con la calidad de los datos de entrenamiento y no tanto con el número de datos usados para entrenarlo. La comunidad de investigadores recomienda tener cómo mínimo un dataset de 1000 puntos cuidadosamente elaborados, en vez de tener 100mil de poca y pobre calidad.
Hacemos el split del Excel
Aplicaremos un procedimiento para partir el archivo de Excel en 3 partes y generar 3 archivos en formato JSONL. Estas tres partes son:
- Training: esta parte contiene el 60% de los datos de entrenamiento y será usada directamente para enseñarle al modelo nuestro estilo o conocimiento adicional, es decir, esto afectará los parámetros del modelo.
- Validation: esta parte contiene el 20% de los datos de entrenamiento. Esta parte es la que le sirve al entrenamiento para validar que tan bien funciona el modelo en un conjunto de datos que no ha visto durante su entrenamiento, es decir, el dataset de validación sirve para conocer qué tan bien generaliza el modelo ante nuevos datos. Este conjunto de datos no afecta los parámetros del modelo, solamente es informativo.
- Test: esta parte contiene el otro 20% de los datos y juega un papel fundamental a la hora de testear el modelo ya no en la fase de entrenamiento sino en la fase de producción.
Crearemos un script en PHP para realizar este split, pero antes vamos a instalar las dependencias para poder leer un XLSX desde PHP:
% brew install php
% php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
% php composer-setup.php
% php -r "unlink('composer-setup.php');"
% ./composer.phar require phpoffice/phpspreadsheet
Creamos el script split.php con el siguiente código que leerá el archivo de Excel dataset.xlsx y lo partirá en 3 archivos en formato JSONL de forma aleatoria:
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
$spreadsheet = new Spreadsheet();
$inputFileType = 'Xlsx';
$inputFileName = './dataset.xlsx';
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile($inputFileName);
$reader->setReadDataOnly(true);
$spreadsheet = $reader->load($inputFileName);
$sheetData = $spreadsheet->getSheetByName('dataset')->toArray(null, true, true, true);
/* Prompt */
$prompt = "Analiza el siguiente mensaje y dime si existe un comportamiento sospechoso ### %s ###";
function create_train_file()
{
global $prompt;
global $sheetData;
if (file_exists("train.jsonl")) unlink("train.jsonl");
if (file_exists("dataset.jsonl")) unlink("dataset.jsonl");
foreach ($sheetData as $key => $value)
{
echo "Procesing Training Event: [" . $key . "]\n";
$query = sprintf($prompt, $value['A']);
$answer = $value['B'];
$result = ['text' => '<start_of_turn>user\n'.$query.'<end_of_turn>\n<start_of_turn>model\n'.$answer.'<end_of_turn>\n'];
$output = json_encode($result, JSON_UNESCAPED_UNICODE) . "\n";
$output = str_replace("\\\\", "\\", $output);
file_put_contents('train.jsonl', $output, FILE_APPEND);
file_put_contents('dataset.jsonl', $output, FILE_APPEND);
}
}
function create_valid_test_file()
{
if (!file_exists('train.jsonl')) die('No train.jsonl file found!');
$train = file_get_contents('train.jsonl');
$trainLines = explode("\n", $train);
$trainLines = array_filter($trainLines);
$totalLines = count($trainLines);
$validationCount = round($totalLines * 0.20);
$testCount = round($totalLines * 0.20);
$trainCount = $totalLines - $validationCount - $testCount;
$indices = range(0, $totalLines - 1);
shuffle($indices);
$valIndices = array_slice($indices, 0, $validationCount);
$testIndices = array_slice($indices, $validationCount, $testCount);
$trainIndices = array_slice($indices, $validationCount + $testCount);
$valLines = [];
$testLines = [];
$newTrainLines = [];
foreach ($indices as $i => $index)
{
if (in_array($index, $valIndices)) $valLines[] = $trainLines[$index];
elseif (in_array($index, $testIndices)) $testLines[] = $trainLines[$index];
else $newTrainLines[] = $trainLines[$index];
}
file_put_contents('train.jsonl', implode("\n", $newTrainLines));
file_put_contents('valid.jsonl', implode("\n", $valLines));
file_put_contents('test.jsonl', implode("\n", $testLines));
}
create_train_file();
create_valid_test_file();
echo "Done! Training, validation and test JSONL files were created.\n"
En este script en PHP se debe ajustar el prompt. Básicamente lo que debe hacer es darle la instrucción al LLM del análisis que usted desee que se realice. Una vez ajuste el prompt debe ejecutar el script para que genere los 3 archivos JSONL:
% php script.php
Los archivos resultantes (train.jsonl, validation.jsonl y test.jsonl) debe colocarlos en una carpeta llamada dataset.
Template de entrenamiento
Como pudo observar en el script split.php, se combinan el prompt, el input y el output para generar una entrada para cada archivo en formato JSONL. Estas entradas deben respetar una plantilla de interacción creada por los desarrolladores de Gemma 3, como se muestra a continuación:
['text'=>'<start_of_turn>user\n PROMPT+INPUT<end_of_turn>\n<start_of_turn>model\n OUTPUT<end_of_turn>\n']
Usted no debe hacer nada adicional respecto a este patrón de interacción ya que el script de PHP hizo todo este trabajo de juntar el prompt, el input y el output en el formato necesario para el entrenamiento.
Hiperparámetros de entrenamiento
Crearemos el archivo config.yaml con los siguientes parámetros de entrenamiento:
model: "./mlx_model"
train: true
fine_tune_type: lora
data: "dataset"
seed: 0
num_layers: -1
batch_size: 4
iters: 500
val_batches: 25
learning_rate: 1e-05
steps_per_report: 5
steps_per_eval: 5
resume_adapter_file: null
adapter_path: "adapters"
save_every: 5
test: false
test_batches: -1
max_seq_length: 2048
grad_checkpoint: false
lora_parameters:
rank: 32
alpha: 64
scale: 10.0
dropout: 0.0
lr_schedule: null
Estamos indicando que el entrenamiento lo realizaremos con los siguientes fundamentos:
- Tipo de finetuning: tipo LoRA, traduce a Low Rank Adaptation y es una técnica de entrenamiento popular y liviana que reduce significativamente la cantidad de parámetros entrenables. Funciona insertando una cantidad menor de nuevos pesos en el modelo y solo estos se entrenan.
- Cantidad de capas: se especifica -1 para decirle a MLX que debe entrenar todas las capas del modelo. En caso de no querer entrenar todas las capas, se debe especificar el número de capas aquí. El valor por defecto es 16.
- Tamaño del batch: hace referencia a la cantidad de pasos en paralelo que dará el sistema de entrenamiento sobre nuestros datos. Un batch_size de 1 significa que pasará punto por punto de manera lineal y un batch_size de 4 significa que lo hará en paralelo 4 veces. Este parámetro de entrenamiento es útil para reducir la cantidad de memoria requerida para entrenar un modelo.
- Velocidad de aprendizaje: cuanto y en qué velocidad LoRA aprende de cada paso de entrenamiento. Se puede decir también que significa cuan lento se lee el dataset de entrenamiento en el proceso de fine tuning. Es parecido a leer un libro, si se lee rápido seguramente se aprende poco y se lee lento se tiene mejor retención y memorización. En este caso se especifica uno de los learning rate mas bajos 1e-05.
- Rank: uno de los hyper parámetros más relevantes a la hora de realizar entrenamiento. Indica cuál será el nivel de profundidad que queremos que el modelo tenga sobre nuestros datos. Es equivalente a compararlo con la calidad de la resolución de una imagen. A mayor rank, mayor detalle y a menor rank, menos aprende detalles.
- Alpha: el impacto que tendrá nuestro fine tuning en todo el modelo. Un valor bajo de alpha permitirá al modelo fundacional seguir generalizando sobre nuevos datos sin que los nuestros tengan mayor relevancia, en cambio, un alpha alto impacta más parámetros del modelo haciendo nuestros datos mucho mas relevantes.
En otros artículo profundizaremos mucho mas sobre estos parámetros, pero para empezar es bueno conocer lo básico del significado de cada uno.
Entrenamiento del modelo
Con lo anterior ya estamos listos para ejecutar el entrenamiento. Lo hacemos con el siguiente comando:
% mlx_lm.lora --train --model ./mlx_model -c config.yaml
Para un Apple Mac Studio M1 Ultra con 64GB de RAM, esto debería tardar aproximadamente 4 horas. Durante ese tiempo el sistema operativo se vuelve inutilizable, por lo cual no debe ejecutar ningún otro tipo de tareas hasta que termine.
Mientras tanto lo que vemos es lo siguiente, iteración por iteración:
Starting training..., iters: 500
Iter 1: Val loss 3.770, Val took 57.861s
Iter 5: Val loss 3.044, Val took 57.845s
Iter 5: Train loss 3.361, Learning Rate 1.000e-05, It/sec 0.165, Trained Tokens 8557, mem 56 GB
Iter 5: Saved adapter weights to adapters/adapters.safetensors.
Iter 10: Val loss 2.526, Val took 58.144s
Iter 10: Train loss 2.669, Learning Rate 1.000e-05, It/sec 0.160, Trained Tokens 17373, mem 58 GB
Iter 10: Saved adapter weights to adapters/adapters.safetensors
Iter 15: Val loss 2.368, Val took 57.714s
Iter 15: Train loss 2.530, Learning Rate 1.000e-05, It/sec 0.048, Trained Tokens 27571, mem 65 GB
Iter 15: Saved adapter weights to adapters/adapters.safetensors
Iter 20: Val loss 2.094, Val took 57.665s
Iter 20: Train loss 2.135, Learning Rate 1.000e-05, It/sec 0.189, Trained Tokens 35004, mem 65 GB
Iter 20: Saved adapter weights to adapters/adapters.safetensors
Al terminar el proceso lo que se obtiene es una carpeta llamada adapters con varios archivos dentro de ella que contienen los pesos del modelo que podrían ser aplicados para cambiar la forma en la que se comporta, dadas nuestras instrucciones.
Mientras el proceso de entrenamiento sucede, podemos ir monitoreando el consumo de recursos:
% macmon

Así se ve el consumo de recursos del Mac M1 Ultra al momento de entrenar la red neuronal. Los procesadores centrales (CPU) no se usan, pero en cambio la potencia computacional del GPU se usa completamente junto con la memoria unificada. La temperatura varía entre 50 y 64 grados centígrados y la energía que consume constantemente es en promedio unos 52 Watts.
La mejor iteración posible
La iteración número 500 seguramente no fue la mejor iteración de este entrenamiento, sin embargo, con seguridad entre la iteración 1 y la 500 debe estar la mejor de todas. En la carpeta adapters deben estar guardadas las iteraciones de 5 en 5 (como especificamos en el archivo config.yaml) y de ahi seleccionaremos la mejor de ella para generar en última instancia nuestro modelo.
Cuál de las iteraciones debemos elegir? por regla general, escogeríamos la iteración donde la pérdida de validación haya sido menor y en donde la pérdida de entrenamiento no esté muy lejos de la pérdida de validación.
Una buena técnica para escoger la mejor iteración consiste en graficar los resultados y de una forma visual poder apuntar al mejor valor. En este caso lo haremos con GNU PLOT, un software libre que crea gráficos:
% brew install gnuplot
Guardaremos todo lo generado por el comando mlx_lm.lora en un archivo llamado train.log y luego, a partir de este archivo, generaremos uno nuevo con solo 3 columnas llamado graph.log:
% cat train.log|sed '$!N;$!N;s/\n/ /g'|awk '{ print $2 $5 $13}'|tr ',' ' '|tr ':' ' ' > graph.log
Esto generará el archivo graph.log con tres columnas, la primera con el numero de línea, la segunda con el valor del validation loss y la tercera con el valor del training loss. Una vez generado este archivo procedemos a crear la gráfica ejecutando gnuplot e ingresando los siguientes comandos:
% gnuplot
> set terminal png size 2200,1600
> set output 'graph.png'
> set xtics 5
> set ytics format "%.3f"
> set ytics 0.050
> set datafile separator space
> set grid
> set ylabel "Loss"
> set xlabel "Iterations"
> set title "Validation/Train Loss"
> plot "graph.log" using 1:2 title 'Vl' with lines, "graph.log" using 1:3 title 'Tl' with lines
Esto habrá generado el archivo graph.png como se muestra a continuación:

Visualizando la gráfica podemos intuir que el mejor ciclo estaría alrededor del número 210, donde la pérdida de validación llegó a su valor mas bajo (1.250) y la pérdida de entrenamiento no está muy lejos (1.255).
Con esta información, seleccionaremos la iteración número 210 con el siguiente comando y la fusionamos al modelo base:
% cp adapters/0000210_adapters.safetensors adapters/adapters.safetensors
% mlx_lm.fuse --model ./mlx_model --adapter-path ./adapters
Procedemos a correr el test sobre esta iteración (aquí cobra relevancia el archivo test.jsonl con el 20% de los datos):
% mlx_lm.lora --model ./fused_model --data dataset --test
Test loss 1.641, Test ppl 5.158.
Generando un muy buen valor de pérdida de testeo sobre la muestra que datos que nunca vio el modelo entrenado. El valor PPL se refiere a “perplexity” que básicamente lo que significa es que nuestro modelo, para generar la siguiente palabra, tiene 5 alternativas posibles, reflejando así su nivel de “confidencia” o seguridad. Detallaremos un poco de esto mas adelante.
Cuantización del modelo
Es hora de convertir el modelo fusionado en formato GGUF y cuantizado a 4 Bits de tal forma que podamos correrlo con facilidad en Ollama. Para ello ejecutamos los siguientes comandos:
% python3.11 llama.cpp/convert_hf_to_gguf.py ./fused_model --outfile model.gguf
% ./llama.cpp/build/bin/llama-quantize model.gguf model-4bit.gguf Q4_K_M
Esto generará el archivo model-4bit.gguf que posteriormente usaremos con Ollama para las inferencias.
Inferencias en Ollama con nuestro modelo
Instalamos Ollama en nuestro Mac Studio visitándo la página de descarga de ollama para Mac OS y siguiendo las instrucciones. Este es un instalador gráfico, es decir, no se ejecuta en línea de comandos sino que se instala dando doble clic en el paquete descargado.
Para correr nuestro modelo en Ollama ejecutamos los siguientes comandos:
% ollama pull gemma3:12b
echo "FROM model-4bit.gguf" > Modelfile
% ollama create -f Modelfile model
% systemctl restart ollama
% ollama run model
Conclusiones y tips
Hemos descargado Gemma 3 a nuestro Mac Studio y lo hemos re-entrenado a partir de un dataset custom, usando las herramientas del framework MLX pero también las herramientas de Meta para la cuantización. Finalmente hemos puesto a correr nuestro modelo con Ollama.
No quisiera terminar este artículo sin antes dar unos tips que seguro serán de mucha ayuda en el proceso de fine tuning:
- En un escenario ideal, ambos, el training loss y el validation loss deberían decrecer a un punto de convergencia indicando así que el modelo está aprendiendo correctamente y es capaz de generalizar sobre datos nuevos y realizar predicciones precisas.
- Escoger como mejor iteración el número mas bajo de validation loss no garantiza que vayamos a tener un buen modelo. Lo único que nos acercará a tener un mejor modelo es nuestro set de testeo que en últimas mostrará que tan bien rinde nuestro modelo ante datos que nunca ha visto (recordemos que durante el training ya vio el dataset del training y del validation pero no el de test).
- Es importante monitorear la relación entre el training loss y el validation loss. En caso de que el training loss continue decreciendo mientras que el validation loss incrementa, puede ser un indicativo de overfitting o sobre-entrenamiento.
- El número de iteraciones necesarias para completar 1 Epoch es igual al número de filas del dataset de entrenamiento dividido por el batch size. Algunos papers recomiendan que el mejor entrenamiento requiere solamente de 1 Epoch (One epoch is all you need).
- La perplejidad o perplexity es una métrica usada cuando se testea el modelo y mide la certeza en las predicciones del modelo. Indica cómo de sorprendido está nuestro modelo al momento de generar texto. Un valor bajo de perplejidad incrementa la confianza en el modelo porque tiene menos opciones en la secuencia de generación de la siguiente palabra. Un valor mas alto agrega confusión y poca confianza al tener muchas opciones posibles para generar la siguiente palabra. Cuando entrenamos un modelo buscamos un equilibro entre confianza o desconfianza (creatividad).
- Un valor recomendado para seleccionar el mejor ciclo es cuando la pérdida de entrenamiento y de validación estén entre 1.2 y 1.3. Valores por debajo de esos significa que nuestro modelo quedó sobre-entrenado y valores por encima significa que le faltó entrenamiento.
- El hiperparámetro rank corresponde al número de parámetros en las capas de adaptación. Entre más parámetros, el modelo recordará mejor los datos y cosas mas complejas podrá manejar. Un valor de rank entre 4 y 16 básicamente le está indicando al modelo solamente un estilo de escritura. Valores por encima de 16, como 32 y 64 le están indicando al modelo que aprenda nuevos conceptos.
- El hiperparámetro alpha es un factor de escalamiento y cambia la forma en la que infiere el modelo base. Un valor alto de alpha significa que las capas de Lora (las nuevas) tienen mas influencia sobre el modelo fundacional.
- El dataset de entrenamiento es el que en última instancia va a determinar la calidad del modelo. Según el tamaño, debe distribuirse de cierta forma en train, validation y test. Si el dataset consiste entre 1.000 y 10.000 filas, la distribución debe ser de 60:20:20. Si el dataset está entre 10.000 y 100.000 filas el ratio debe ser de 70:15:15 y si el modelo es mayor de 100.000 el ratio debe ser de 80:10:10.
Espero que este artículo y los tips sean de mucha utilidad. Nos veremos en una próxima entrega donde profundizaremos mucho más en el funcionamiento de LoRA y cómo a través de fórmulas matemáticas podemos establecer mejores valores para Rank y Alpha.
Referencias
Acerca de NOFRAUD
NOFRAUD es la compañía que desarrolla el software antifraude The Fraud Explorer y apoya a personas y empresas a enfrentar y solucionar sus retos en materia de fraude interno, corrupción y abuso corporativo. NOFRAUD ha creado la base de datos conductual de actos deshonestos más grande del mundo en Español e Inglés, que sirve para que la inteligencia artificial encuentre patrones sospechosos de corrupción al interior de las organizaciones.

Mejoramos la capacidad de las organizaciones incrementando sus beneficios, arrebatándole a los perpetradores la posibilidad de afectar negativamente los ingresos a través del fraude, la corrupción, el abuso corporativo y la generación de ambientes tóxicos.
Contacte conmigo en » jrios@nofraud.la y Visítenos en » www.nofraud.la.