• Updated:
  • Featured post

How LLMs generate the next token

(este post es una explicación de la teoría. dejo aquí otro post con el detalle práctico de como usar la temperature, top_k y top_p a efectos prácticos)

Explicación más en detalle de cómo obtienen los LLMs las probabilidades para generar el siguiente token

Sampling

En cada posición el modelo tiene una bolsa con miles de tokens posibles. Sampling es el proceso de no coger siempre el token más probable, si no de orientarlo a coger respuestas con determinadas características.

Si el modelo escogiera siempre el token con más probabilidades, obtendríamos respuestas aburridas y repetitivas.

Logits

Para generar el siguiente token, una red neuronal calcula primero los vectores de logits, donde cada logit corresponde a un valor posible. El tamaño de estos vectores de logits es tan grande como el vocabulario completo del modelo.

(representación de vectores de logits)

flowchart LR
	N1["What's your favorite color?"]:::note --> Z
	Z --> A1 --> A
	Z --> B1 --> B
	Z --> C1 --> C
	Z --> D1 --> D
	
	Z["Neural network"]
    A["a"]
    A1["(-0.5)"]
    B["green"]
    B1["(0.7)"]
    C["red"]
    C1["(0.5)"]
    D["the"]
    D1["(-1.2)"]
    
    classDef note fill:none,stroke:none,color:#777;    

Los logits NO representan probabilidades ya que no suman 1 y pueden incluso ser negativos (la probabilidades no pueden). Para convertir logits a probabilidades se usa una Softmax layer

Temperature

La temperatura es una constante que se aplica a los logits antes de la transformación de la Softmax layer. Se usa para ajustar la creatividad del modelo y redistribuir la probabilidad de los valores. Una temperatura más alta hace que el modelo sea más creativo ya que aumenta las posibilidades de elegir tokens menos probables.

xychart-beta
  title "Temperatura vs Probabilidad"
  x-axis "Temperatura (T)" [0.1, 0.2, 0.5, 1, 2, 5]
  y-axis "Probabilidad" 0 --> 1
  line "P(token1)" [0.9999546, 0.9933071, 0.8807971, 0.7310586, 0.6224593, 0.5498340]
  line "P(token2)" [0.0000454, 0.0066929, 0.1192029, 0.2689414, 0.3775407, 0.4501660]

Ejemplos de temperaturas:

  • Low (0.2-0.3): El modelo es cauto y elige las palabras más probables. Output factual y predecible.
  • Medium (0.5-0.7): Un mix de confiabilidad y engagement
  • High (0.9-1.0): Toma riesgos y es impredecible

Read More

  • Updated:

Moq (It.isAny & mock multiple calls)

Mocking examples with Moq

It.IsAny<T> variables

Those are argument matchers we have in Moq for the case we don’t have the same instance match.

Exact instance match - most restrictive

Example for default comparison

[Theory, Autodata]
public async Task FilledString_Execute_CorrectlyProcessed(string name, AnimalDTO animal)
{
	// ARRANGE
	_dependencyMock
		.Setup(m => m.Load(name))
		.Returns(animal)
	
	// ACT
	AnimalDTO result = await _sut.Execute(name);
	
	// ASSERT
	_dependencyMock.Verify(m => m.Load(name));
}

It.IsAny<T> - most permissive

Matches any instance of that type, null included.

[Theory, Autodata]
public async Task FilledString_Execute_CorrectlyProcessed(string name, AnimalDTO animal)
{
	// ARRANGE
	_dependencyMock
		.Setup(m => m.Load(name))
		.Returns(animal)
	
	// ACT
	AnimalDTO result = await _sut.Execute(name);
	
	// ASSERT	
	// we use this in case the property is not accesible
	//   for example: a param created inside the class
	_dependencyMock.Verify(m => m.Load(It.IsAny<string>()));
}

It.Is<T>(predicate)

Matches only when the predicate is true.

[Theory, Autodata]
public async Task FilledString_Execute_CorrectlyProcessed(string name, AnimalDTO animal)
{
	// ARRANGE
	_dependencyMock
		.Setup(m => m.Load(name))
		.Returns(animal)
	
	// ACT
	AnimalDTO result = await _sut.Execute(name);
	
	// ASSERT
	_dependencyMock.Verify(m => m.Load(It.Is<AnimalDTO>(a => a.Name == name)));
}

It.IsNotNull<T>

Only checks something is not null

[Theory, Autodata]
public async Task FilledString_Execute_CorrectlyProcessed(string name, AnimalDTO animal)
{
	// ARRANGE
	_dependencyMock
		.Setup(m => m.Load(name))
		.Returns(animal)
	
	// ACT
	AnimalDTO result = await _sut.Execute(name);
	
	// ASSERT
	_dependencyMock.Verify(m => m.Load(It.IsNotNull<AnimalDTO>()));
}

Mock multiple calls with same params

This is an example on how to mock a call when it’s called multiple times, and with the same parameter type every time.

Setup

I have the following class…

public class ConnectorRequest
{
	public string Query { get; set; }
}

… which will be consumed by the following service

public class IConnectorService
{
	Task<string> Execute(ConnectorRequest request);
}

Then I have a class which calls IConnectorService multiple times

public class ConnectorConsumerService
{
	private IConnectorService _service;
	
	// ...
	
	public async Task<string> Process() 
	{
		// ... does whatever
		var response1 = await _service.Execute(request1);
		// ... does whatever with that information
		var response2 = await _service.Execute(request2);
		// ... does whatever with that information
		var response3 = await _service.Execute(request3);
		// ... does whatever with that information
		// ... does whatever else
	}
	
	// ...
	
}

Test

Test which mocks multiple calls

public class ConnectorConsumerServiceTest
{
	// all mocks and stubs
	private Mock<IConnectorService> _dependencyMock;

	// service under test
	private ConnectorConsumerService _service;

	public ConnectorConsumerServiceTest()
	{
		_dependencyMock = new Mock<IConnectorService>();
		_service = new ConnectorConsumerService(_dependencyMock.Object);
	}

	[Fact]
	public async Task ProcessXXX_CaseXXX_ShouldReturnOkay()
	{
		// ARRANGE
		// example starts here! -> 
		var responseToExecution1 = new ConnectorRequest
		{
			Query = "some response";
		}
		var responseToExecution2 = new ConnectorRequest
		{
			Query = "another response";
		}
		var responseToExecution3 = new ConnectorRequest
		{
			Query = "oh no! a response";
		}
		
		_dependencyMock.SetupSequence(mock => mock.Execute(It.IsAny<ConnectorRequest>()))
			.ReturnsAsync(responseToExecution1)
			.ReturnsAsync(responseToExecution2)
			.ReturnsAsync(responseToExecution3);
		// <- example ends here

		// ACT
		var result = await _service.Process();
	
		// ASSERT
		result.status.Should().NotBeNull();
		// ... assert whatever
	}
}
  • Updated:

AI Data Grouding (RAG vs Fine Tuning)

TLDR:

  • RAG: complementar el prompt referenciando conocimiento concreto, actualizado o propietario
  • Fine-tuning: reentrenar el modelo para que cambie su estilo, tono o formato - altamente complejo. Sólo después de haber probado otros métodos.

El objetivo de customizar los modelos es mejorar aspectos de su performance, calidad y/o seguridad de sus respuestas.

Data Grounding

Proceso de enriquecer una respuesta generada por IA con datos externos o específicos para mejorar su calidad y/o seguridad de sus respuestas. Asegura que el output del modelo está alineado con datos factuales, contextuales y que son confiables.

Data Grounding es el objetivo, no una técnica.

Para responder al prompt la IA no se basará sólo y únicamente en los datos de su entrenamiento, si no que se le proporciona información adicional de una fuente externa (documentos, BBDD, APIs, etc.) para que sus respuestas estén basadas en datos reales y actualizados y no se los invente ni alucine.

RAG vs Fine-Tuning

¿Necesitas datos up-to-the-minute o acceso a datos privados? Por lo general se recomienda comenzar con RAG. Es más rápido de implementar y necesita menos datos que fine-tuning.

¿Necesitas que la IA hable/escriba en un estilo muy específico o que entienda un nicho de manera profunda? - Hacer fine-tuning. Ayuda al modelo a aprender patrones y estilos que solamente con RAG son muy difíciles de capturar.

¿Necesitas hacer tareas muy complejas que requieren datos específicos / actualizados y un conocimiento profundo? - Hacer ambos (Fine-tuning+RAG).

Read More

  • Updated:

AI Base Concepts

Foundation Models

Los foundation models son modelos de IA generalistas entrenados con datasets diversos y masivos, que están listos para ser adaptados a muchos tipos de tareas diferentes.

Sobre todo NO están limitados a lenguaje escrito. Pueden trabajar con cualquier variación de: Texto, Imagen, Audio, Video y Código.

LLM (Large Language Model)

Los LLMs son sistemas estocásticos (no deterministas) entrenados para generar predicciones de texto basadas en prompts. Sus datos de entrenamiento se basan sobre todo en texto / código y se especializan en lectura / escritura y lenguaje oral. El truco está en que el modelo entienda la relación semántica entre palabras, y qué palabras de una secuencia son las que tienen más probabilidades de influir en la siguiente; el modelo usa esto para predecir cual es la siguiente palabra más probable en la secuencia.

Un LLM no tiene “memoria” como tal. Las conversaciones como tal no existen para ellos. Cada input de una conversación contiene todo lo que se ha hablado antes.

Al igual que tenemos LLMs, también existen SLMs (small language models). La diferencia se basa en el volumen de datos con el que han sido entrenados y en el número de variables, aunque no hay límites definidos.

Hay dos tipos de modelos de lenguaje (Aunque en principio un LLM se entrene para un tipo, esto se puede cambiar después mediante fine-tuning (ver infilling fine-tuning)):

  • Autoregressive: predice el siguiente token de una secuencia, usando los tokens previos como contexto. (My favorite color is _ )
  • Masked: fill-in-the-blank. Rellena un hueco usando como contexto los tokens que vienen antes y los de después (The _ is blue)

Ventana de contexto (Context Window)

La ventana de contexto es el limite de memoria que tiene un LLM. Determina cuánto de la conversación actual puede “mantener en memoria” el modelo.

Si la ventana es suficiente, el modelo podrá mantener la totalidad de la conversación en memoria, pero si nos pasamos del límite, el modelo comenzará a olvidarse de las primeras partes de la conversación, y comenzará a dar respuestas más vagas o alucinar.

Tokenization

El vocabulario de los LLMs se basa en cientos de miles de tokens, los cuales se basan en graaaandes volúmenes de datos de entrenamiento.

Los tokens se componen de palabras (perro, gato) pero también de partículas (“in” de “innecesario” o de “incomprensible”), puntuación (“casa” y “casa.” son tokens diferentes) y otras secuencias de caracteres.

Ejemplo de tokenization:

  • I (1)
  • heard (2)
  • a (3)
  • dog (4)
  • bark (5)
  • at (6)
  • a (3) (already assigned before)
  • tree (8)

Cuantos más datos de entreno, más tokens y más vocabulario tendrá.

Transforming tokens with a transformer

Ahora que tenemos una serie de tokens con un id único. Tenemos que relacionarlos entre ellos.

Para esto, le asignamos a cada token un vector. Un vector es una lista de números con múltiples dimensiones.
Un ejemplo de vector puede ser [0.25, 0.88, -0.47, 0.91]

Una vez tenemos inicializado este vector con valores aleatorios, utilizamos las dimensiones del vector para hacer un encoding lingüístico y asignarles el valor semántico del token (qué significa y como se relaciona con otros tokens).

Because this new vectors have semantic values embedded in them, we call them embeddings

Read More

Fluent Assertions examples

Examples on how to test and assert exceptions with FluentAssertions.

with * it checks it contains the string. if you want exact matching, remove them

[Fact]
public async Task CreateAnimal_IsNull_ExceptionThrown()
{
	// ARRANGE 
	Animal? animal = null;
	
	// ACT 
	var act = async () => await sut.Create(animal);
	
	// ASSERT
	await act.Should().ThrowAsync<ArgumentNullException>()
		.WithMessage("*is null*");
}

AutoFixture (AutoData & AutoMoq)

AutoFixture is a .NET library to automatically generate test data. To use it install the following packages

AutoFixture
AutoFixture.Xunit2
AutoFixture.AutoMoq

Inyecting with AutoData

You can create the fixtures either manually or automatically. I do it automatically with [AutoData]

test class

public class AnimalControllerTests
{
	private readonly Mock<AnimalService> _serviceMock;
	private readonly AnimalController _sut;
	
	public AnimalControllerTests()
	{
		_serviceMock = new Mock<AnimalService>();
		_sut = new AnimalController(_serviceMock.Object);
	}
}

test method example

[Theory, AutoData]
public async Task Animal_Process_IsCorrectlyCreated(Animal animal, Guid guid)
{
	// when calling this test, Animal and Guid have already been inyected
	
	// ARRANGE
	_serviceMock.Setup(x => x.Create(animal))
		.Returns(guid);
	
	// ACT
	var resultGuid = await _sut.Process(animal);
	
	// ASSERT
	_serviceMock.Verify(m => m.Create(animal));
	resultGuid.Should().Be(guid);
}

Read More

XUnit tags and usage

Fact vs Theory

[Fact] son tests que se ejecutan siempre con los mismos valores fijos

[Fact]
public void Sum_TwoNumbers_ReturnsCorrectResult()
{
	// Act
	var result = 1 + 2;
	
	// Assert
	result.Should().Be(3);
}

[Theory] son tests parametrizados que se ejecutan múltiples veces con datos diferentes. Requieren de otro modificador que provea los valores

[Theory]
[InlineData(1, 2, 3)]
[InlineData(0, 0, 0)]
[InlineData(-1, 1, 0)]
public void Sum_TwoNumbers_ReturnsCorrectResult(int a, int b, int expected)
{
	// Act
	var result = a + b;
	
	// Assert
	result.Should().Be(expected);
}

Read More

  • Updated:

Testing. Code example for c# (NUnit)

(this project contains examples on how to use NUnit. Nowadays it’s better to use XUnit for new projects).

The following are code examples to test several scenarios.

(check this project to see more testing code examples)

General tests

This is a test to check an exception is thrown for a non-async method.

The method .WithMessage("*null payload*") works as a like. Any string that contains null payload will trigger as true.

[Test]
public async Task GivenNullPayload_WhenSetupPost_ThenAssertExceptionIsThrown
{
	// given
	string url = "http://somethingsomething.com"
	string nullPayload = null;
	
	// when
	Action result = () => _service.SetUpPost(url, nullPayload);
	
	// then
	result.Should().Throw<ArgumentNullException>().WithMessage("*null payload*");
}

This is the example for an async method.

[Test]
public async Task GivenNullPayload_WhenSetupPostAsync_ThenAssertExceptionIsThrown
{
	// given
	string url = "http://somethingsomething.com";
	string nullPayload = null;
	
	// when
	Func<Task> result = () => _service.SetUpPostAsync(url, nullPayload);
	
	// then
	await result.Should().ThrowAsync<InvalidOperationException>().WithMessage("*null payload*");
}

Read More

  • Updated:

LLM Hyperparameters

Los hyperparámetros son settings de configuración, los cuales se configuran manualmente para optimizar la eficiencia o la creatividad de la respuesta

Temperature (creatividad)

La temperatura controla probabilidad de distribución del modelo a la hora de elegir las siguientes palabras:

xychart-beta
  title "Temperatura vs Probabilidad"
  x-axis "Temperatura (T)" [0.1, 0.2, 0.5, 1, 2, 5]
  y-axis "Probabilidad" 0 --> 1
  line "P(token1)" [0.9999546, 0.9933071, 0.8807971, 0.7310586, 0.6224593, 0.5498340]
  line "P(token2)" [0.0000454, 0.0066929, 0.1192029, 0.2689414, 0.3775407, 0.4501660]
  • Low (0.2-0.3): El modelo es cauto y elige las palabras más probables. Output factual y predecible.
  • Medium (0.5-0.7): Un mix de confiabilidad y engagement
  • High (0.9-1.0): Toma riesgos y es impredecible

Respuesta posibles con la siguiente frase

Once upon a time, there was a dragon…

  • Temp 0.2: …who lived in a cave guarding treasure
  • Temp 0.7: …who dreamed of becoming friends with the villagers
  • Temp 1.0: …who loved baking cookies and singing karaoke

Cuanta más alta la temperatura, más imaginativo, creativo y loco es el modelo, lo cual no siempre es bueno. Para usos creativos se suele usar una temperatura de 0.7.

Top-k (candidate pool fija)

Top-k es una estrategia de sampling para filtrar el número de candidatos a los más probables. Fuerza al modelo a elegir el siguiente token sobre una pool k de candidatos.

Es una estrategia de sampling de tamaño fijo, una vez tenemos computados los logits. Ayuda a reducir el número de candidatos sobre los que se calculan la softmax layer.

Candidatos posibles con la siguiente frase

I like to drink…

  • small k (5): El modelo solo tiene unas pocas elecciones posibles, las cuales serán las más seguras posibles. Ej: [water, coffee, tea, juice, milk]
  • large k (50): El modelo puede elegir entre opciones muy amplias. Amplía la variedad pero también la posibilidad de que salgan resultados inesperados Ej: […, smothies, kombucha, cocktails, hot chocolate, …]

(!) La contra que tiene es que si el modelo está muy seguro, puede incluir tokens con una baja probabilidad solo por el hecho de que tiene que llegar hasta k candidatos. (!)

Read More