Advent of Code 2022 in pure TensorFlow - Day 6


Solving problem 6 of the AoC 2022 in pure TensorFlow allows us to understand how powerful this framework can be. In particular, problem 6 can be solved with a highly efficient and parallel solution, using just a single feature of tf.data.Dataset: interleave.

Day 6: Tuning Trouble

You can click on the title above to read the full text of the puzzle. The TLDR version is: we need to decode a signal. The signal is a string containing some “random” characters. Decoding a signal means detecting a marker character. A marker character is defined as the first character of a sequence of 4 (part 1) or 14 (part 2) characters without repeater characters inside.

So, given a puzzle input like

mjqjpqmgbljsphdztnvjfqwrcgsmlb

we need to analyze the signal sequentially (left to right) and search for the first sequence of 4 characters that are all different. In this case, we start from the left mjq are the first 3 characters. The 4 characters, however, is a j that’s contained in the mjq sequence, so j is repeated and thus m is not a marker character. The first time a marker appears is after the seventh character arrives. In this case, the last four characters received are jpqm, which are all different. Thus, the result of the analysis is 7.

Part 1 asks us to detect the marker character considering sequences of 4 different characters, part 2 instead requires sequences of 14 different characters.

Design Phase

The problem may look complicated since it requires searching for sequences of different characters on strings that can potentially overlap. For example, given the sample input

mjqjpqmgbljsphdztnvjfqwrcgsmlb

The first search fails. mjqj is not a valid sequence. Thus, we need to restart the search from the first j character of the sequence, finding jqjp that’s once again not correct. We need to repeat this very same algorithm until we don’t find the jpqm string that satisfies the condition.

There’s a thing to note that will help in designing a fast solution for this problem: every search is potentially independent of each other. If we can split the input sequence into various sub-strings like (for part 1, 4 splits, for part 2, 16 splits):

  • [0,4] -> [4,8] -> [8,12] -> ...
  • [1,5] -> [5,9] -> [9-13] -> ...
  • [2,6] -> [6,10] -> [10-14] -> ...
  • [3,7] -> [7,11] -> [11-15] -> ...

and interleave the sub-strings generating the sequence [0,4] -> [1,5] -> [2,6] -> [3,7] -> [4,8] -> ..., we can loop over this sequence and stop when the correct substring meets the criteria (all the characters are different).

Understanding tf.data.Dataset interleave

tf.data.Dataset.interleave is the superhero of data transformation. This is the method signature

interleave(
    map_func,
    cycle_length=None,
    block_length=None,
    num_parallel_calls=None,
    deterministic=None,
    name=None
)

The interleave method allows us to apply a transformation (map_func) to an input dataset, generate a new dataset for every iteration, control the behavior of every dataset, and interleave the results into a single output stream of a new dataset object.

The cycle_length and block_length arguments control the order in which elements are produced. The num_parallel_calls and deterministic parameters control the multi-thread behavior of the transformation. When num_parallel_calls is specified, the cycle_lenght elements produced from the initial dataset, are processed by num_parallel_calls threads. This processed data is then grouped in block_length elements and produced as output.

In short, you can think about the block_length parameter as the number of elements that the interleaved dataset will produce on every iteration, while cycle_length is the number of elements for every generated dataset that will be processed concurrently. You can specify the concurrency level through the num_parallel_calls parameter and with the deterministic parameter you can control that every iteration of the dataset respects your deterministic, intended, behavior. In our case, we are interested in having a deterministic approach, since the position of the marker character is important, but of course, there are problems in which you just want to apply transformations to datasets and interleave the results, without being interested in the order of the interleaving.

Solving the problem

tf.data.Dataset.interleave is all we need to solve this problem. With a correct configuration, it can model exactly the behavior described in the design phase section.

The dataset, however, requires to be converted from a single long string (the input signal) to a real “stream” of characters, that we can use as input dataset for our interleave transformation.

chars = tf.convert_to_tensor(
    next(
        dataset.map(lambda line: tf.strings.bytes_split(line))
        .take(1)
        .as_numpy_iterator()
    )
)

dataset = tf.data.Dataset.from_tensors(tf.reshape(chars, [-1, 1])).unbatch()

dataset now is a tf.data.Dataset that produces characters on every iteration (a real stream!). So, how can we create an interleaved version of this dataset that produces the sequence of sub-strings we are interested in?

We should be able to produce 4 (or 16 for part 2) new datasets, each of them starting from a different offset.

  • Dataset 1. Offset 0: mjqj - pqmg - bljs
  • Dataset 2: Offset 1: jqjp - qmgb - ljsp
  • Dataset 3: Offset 2: qjpq - mgbl - jsph
  • Dataset 4: Offset 3: jpqm - gblj - sphd

Using the interleave method is quite easy: we just need to create the right dataset of offsets and generate the interleaved datasets. This dataset will be then used by the interleave method, as specified by its configuration, to produce the desired result.

interleaved = tf.data.Dataset.range(4).interleave(
    lambda offset: dataset.skip(offset).batch(4),
    cycle_length=4,
    block_length=1,
    num_parallel_calls=4,
    deterministic=True,
)

Yes, it really is that easy! With tf.data.Dataset.range(4) we are generating the dataset that produces the values from 0 to 4 sequentially. This dataset is used to produce the offset value for the dataset.skip method invoked as the transformation to the input dataset. So, our map_func produces a new tf.data.Dataset on every iteration of the range-dataset. Every dataset then extracts a batch of 4 elements (the substrings).

The configuration, allows us to iterate over the interleaved 4 datasets, in a deterministic way, extracting on every iteration a batch of 4 elements for each created dataset, interleaved as we expect.

Thus, to completely solve the problem we have to loop over this dataset, check for the uniqueness of the elements in the loop, and get the char’s index:

for count, b in enumerate(interleaved):
    y, _ = tf.unique(tf.reshape(b, -1))
    if tf.equal(tf.shape(y)[0], 4):
        tf.print(y)
        # 1: starts from 0
        # 3: the remaining chars in the sequence
        tf.print("unique found at char: ", count + 4)
        break

Here we go, day 6 problem solved in pure TensorFlow! Solving part 2 is identical, just replace every occurrence of 4 with 14.

Give a look at the complete solution.

Conclusion

You can see the complete solutions in folder 6 in the dedicated GitHub repository (in the 2022 folder): https://github.com/galeone/tf-aoc.

Solving problem 6 allowed us to use a very powerful feature of tf.data.Dataset: interleave. In a few lines, this method allows us to define a complete, highly parallel, and efficient data transformation pipeline, that allows us to transform and group data gathered from different datasets. The expressive power of this method, moreover, allowed us to solve the problem in a very elegant way IMHO.

If you missed the article about the previous days’ solutions, here’s a handy list

For any feedback or comment, please use the Disqus form below - thanks!

Don't you want to miss the next article? Do you want to be kept updated?
Subscribe to the newsletter!

Related Posts

Fixing the code signing and notarization issues of Unreal Engine (5.3+) projects

Starting from Unreal Engine 5.3, Epic Games added support for the so-called modern Xcode workflow. This workflow allows the Unreal Build Tool (UBT) to be more consistent with the standard Xcode app projects, and to be compliant with the Apple requirements for distributing applications... In theory! 😅 In practice this workflow is flawed: both the code signing and the framework supports are not correctly implemented, making the creation of working apps and their distribution impossible. In this article, we'll go through the problems faced during the packaging, code signing, and notarization of an Unreal Engine application on macOS and end up with the step-by-step process to solve them all.

The (Hidden?) Costs of Vertex AI Resource Pools: A Cautionary Tale

In the article "Custom model training & deployment on Google Cloud using Vertex AI in Go" we explored how to leverage Go to create a resource pool and train a machine learning model using Vertex AI's allocated resources. While this approach offers flexibility, there's a crucial aspect to consider: the cost implications of resource pools. This article details my experience with a sudden price increase in Vertex AI and the hidden culprit – a seemingly innocuous resource pool.

Building a RAG for tabular data in Go with PostgreSQL & Gemini

In this article we explore how to combine a large language model (LLM) with a relational database to allow users to ask questions about their data in a natural way. It demonstrates a Retrieval-Augmented Generation (RAG) system built with Go that utilizes PostgreSQL and pgvector for data storage and retrieval. The provided code showcases the core functionalities. This is an overview of how the "chat with your data" feature of fitsleepinsights.app is being developed.

Using Gemini in a Go application: limits and details

This article explores using Gemini within Go applications via Vertex AI. We'll delve into the limitations encountered, including the model's context window size and regional restrictions. We'll also explore various methods for feeding data to Gemini, highlighting the challenges faced due to these limitations. Finally, we'll briefly introduce RAG (Retrieval-Augmented Generation) as a potential solution, but leave its implementation details for future exploration.

Custom model training & deployment on Google Cloud using Vertex AI in Go

This article shows a different approach to solving the same problem presented in the article AutoML pipeline for tabular data on VertexAI in Go. This time, instead of relying on AutoML we will define the model and the training job ourselves. This is a more advanced usage that allows the experienced machine learning practitioner to have full control on the pipeline from the model definition to the hardware to use for training and deploying. At the end of the article, we will also see how to use the deployed model. All of this, in Go and with the help of Python and Docker for the custom training job definition.

Integrating third-party libraries as Unreal Engine plugins: solving the ABI compatibility issues on Linux when the source code is available

In this article, we will discuss the challenges and potential issues that may arise during the integration process of a third-party library when the source code is available. It will provide guidance on how to handle the compilation and linking of the third-party library, manage dependencies, and resolve compatibility issues. We'll realize a plugin for redis plus plus as a real use case scenario, and we'll see how tough can it be to correctly compile the library for Unreal Engine - we'll solve every problem step by step.

AutoML pipeline for tabular data on VertexAI in Go

In this article, we delve into the development and deployment of tabular models using VertexAI and AutoML with Go, showcasing the actual Go code and sharing insights gained through trial & error and extensive Google research to overcome documentation limitations.

Advent of Code 2022 in pure TensorFlow - Day 12

Solving problem 12 of the AoC 2022 in pure TensorFlow is a great exercise in graph theory and more specifically in using the Breadth-First Search (BFS) algorithm. This problem requires working with a grid of characters representing a graph, and the BFS algorithm allows us to traverse the graph in the most efficient way to solve the problem.

Advent of Code 2022 in pure TensorFlow - Day 11

In this article, we'll show how to solve problem 11 from the Advent of Code 2022 (AoC 2022) using TensorFlow. We'll first introduce the problem and then provide a detailed explanation of our TensorFlow solution. The problem at hand revolves around the interactions of multiple monkeys inspecting items, making decisions based on their worry levels, and following a set of rules.

Advent of Code 2022 in pure TensorFlow - Day 10

Solving problem 10 of the AoC 2022 in pure TensorFlow is an interesting challenge. This problem involves simulating a clock signal with varying frequencies and tracking the state of a signal-strength variable. TensorFlow's ability to handle complex data manipulations, control structures, and its @tf.function decorator for efficient execution makes it a fitting choice for tackling this problem. By utilizing TensorFlow's features such as Dataset transformations, efficient filtering, and tensor operations, we can create a clean and efficient solution to this intriguing puzzle.