Using tensors for representing and manipulating data is very convenient. This representation allows changing shape, organizing, and applying generic transformations to the data. TensorFlow - by design - executes all the data manipulation in parallel whenever possible. The day 4 challenge is a nice showcase of how choosing the correct data representation can easily simplify a problem.

The challenge itself is not complicated, but similarly to day 3, we’ll need to use a very dynamic element offered by the framework, the `TensorArray`

data structure.

## Day 4: Giant Squid: part one

You can click on the title above to read the full text of the puzzle. The TLDR version is:

Let’s play bingo! Our puzzle input is a text file containing in the first line a comma separated list of drawn numbers, like

```
7,4,9,5,11,17,23,2,0,14,21,24,10,16,13,6,15,25,12,22,18,20,8,19,3,26,1
```

While the rest of the file contains the `5x5`

boards. Each board is a made of 5 **rows** and 5 **columns** and each board is separated with an empty line.

```
22 13 17 11 0
8 2 23 4 24
21 9 14 16 7
6 10 3 18 5
1 12 20 15 19
3 15 0 2 22
9 18 13 17 5
19 8 7 25 23
20 11 10 24 4
14 21 16 12 6
14 21 17 24 4
10 16 15 9 19
18 8 23 26 20
22 11 13 6 5
2 0 12 3 7
```

The challenge consists in simulating the game. We need, thus, to keep track for every board of the drawn numbers. A player wins the game when one of its board has at least one **complete row or column** of marked numbers.

The text asks to calculate the **score** of the winning board as **the sum of all the unmarked numbers** on that board, multiplied by **the number that was just called**.

In the example, the last one is the winning board, and **bingo** has been made after drawing 7,4,9,5,11,17,23,2,0,14,21, and 24. Thus the sum of all the **unmarked** numbers is `188`

and the final score is `188 * 24 = 4512`

.

### Design phase

We need to simulate the complete game. Therefore we need some good representation of the boards that should allow us to easily find rows and columns with marked numbers. Representing every board as a `tf.Tensor`

with `shape=(5,5)`

can be the best choice.

We also need to find a way to **keep track** for every board of the drawn numbers. The TensorArray data structure, as shown in Day 3, is the perfect fit. This data structure allows us to store arbitrary-shaped `tf.Tensor`

objects, thus we can easily create an array of `5x5`

boards.

We are **not** interested in the value of the marked numbers, hence we can overwrite them with some value that will never be drawn (-1). After every extraction, we should just check if there’s a winner board, for stopping the execution and compute the score of the winning board.

The data representation we use is perhaps the most important part of the solution, and it’s all created in the data pipeline.

### Input pipeline

We create a `tf.data.Dataset`

object for reading the text file line-by-line as usual. But this time, since the data is heterogeneous, we create two different datasets. One dataset produces the drawn numbers, and the other produces the boards.

```
dataset = tf.data.TextLineDataset("input")
# The first row is a csv line containing the number extracted in sequence
extractions = (
dataset.take(1)
.map(lambda line: tf.strings.split(line, ","))
.map(lambda strings: tf.strings.to_number(strings, out_type=tf.int64))
.unbatch()
)
# All the other rows are the boards, every 5 lines containing an input
# is a board. We can organize the boards as elements of the dataset, a dataset of boards
boards = (
dataset.skip(1)
.filter(lambda line: tf.greater(tf.strings.length(line), 0))
.map(tf.strings.split)
.map(
lambda string: tf.strings.to_number(string, out_type=tf.int64)
) # row with 5 numbers
.batch(5) # board 5 rows, 5 columns
)
```

`extractions`

is a dataset that produces a scalar on every iteration. The `unbatch`

method is used to unpack a single `tf.Tensor`

containing all the numbers to a sequence of scalars.
It’s worth noting that `tf.strings.to_number`

is applied to a Tensor containing all the numbers in `tf.string`

format, hence this application is executed in parallel on all the values.

`boards`

is a dataset created skipping the first line (previously used), and by removing all the empty lines. The usual string to integer conversion is applied and we conclude by calling `batch(5)`

. Since every read line contains `5`

elements, by batching them together we end up with a dataset that produces a tensor with a `5x5`

shape on every iteration.

### Computing the score

In the design phase, we decided to put a `-1`

on the board where there’s a match. Hence, we can easily define a helper function that computes and prints the score of a board.

```
def _score(board, number):
tf.print("Winner board: ", board)
tf.print("Last number: ", number)
# Sum all unmarked numbers
unmarked_sum = tf.reduce_sum(
tf.gather_nd(board, tf.where(tf.not_equal(board, -1)))
)
tf.print("Unmarked sum: ", unmarked_sum)
final_score = unmarked_sum * number
tf.print("Final score: ", final_score)
```

The function requires the board to use and the last drawn number to compute the final score. Summing all the unmarked numbers is trivial, in fact, it’s just a matter of filtering for all the values different from `-1`

, gathering the values, and passing them to `tf.reduce_sum`

that when used without specifying the reduction dimension will sum up all the values in the input tensor producing a scalar value.

The final score is then easily computed.

### Playing bingo

Our TensorFlow program needs a state. In particular, we need to know when to stop looping over the extractions (a boolean state), save the last drawn number, and also the winner board. These states should also be returned by our TensorFlow program, so to use the `_score`

function previously defined.

Moreover, we need a `TensorArray`

object for storing in a mutable data structure the various boards. I want to stress out that the `TensorArray`

is one of the few **totally mutable objects** TensorFlow provides. Differently from `tf.Variable`

the elements of a `tf.TensorArray`

can change shape, and the array can grow past its original size.

```
class Bingo(tf.Module):
def __init__(self):
# Assign every board in a TensorArray so we can read/write every board
self._ta = tf.TensorArray(dtype=tf.int64, size=1, dynamic_size=True)
self._stop = tf.Variable(False, trainable=False)
self._winner_board = tf.Variable(
tf.zeros((5, 5), dtype=tf.int64), trainable=False
)
self._last_number = tf.Variable(0, trainable=False, dtype=tf.int64)
```

We need to decide when to stop the extraction loop, for doing so it will be useful to design an `is_winner`

function that given a board checks if it has a row or a column full with marked objects.

During the design phase, we decided to apply `-1`

in the marked position, hence the `is_winner`

function can be easily defined as follows.

```
@staticmethod
def is_winner(board: tf.Tensor) -> tf.Tensor:
rows = tf.reduce_sum(board, axis=0)
cols = tf.reduce_sum(board, axis=1)
return tf.logical_or(
tf.reduce_any(tf.equal(rows, -5)), tf.reduce_any(tf.equal(cols, -5))
)
```

The `axis`

parameter of `tf.reduce_sum`

defines the reduction dimension to use. In practice, we are summing along the rows and the column (respectively in the two calls) getting two tensors with shape `(5)`

containing the sum over these dimensions. If we marked a row/column, then one of these 5 values will be a `-5`

.

Differently from what we made for solving the day 4 puzzle, where we only used the `stack`

and `unstack`

method of `tf.TensorArray`

, this time we use only `unstack`

to populate the `TensorArray`

with the boards, and then we used `read`

and `write`

methods to read/write singularly every board.

```
# @tf.function
def __call__(
self, extractions: tf.data.Dataset, boards: tf.data.Dataset
) -> Tuple[tf.Tensor, tf.Tensor]:
# Convert the datasaet to a tensor and assign it to the ta
# use the tensor to get its shape and know the number of boards
tensor_boards = tf.convert_to_tensor(list(boards)) # pun intended
tot_boards = tf.shape(tensor_boards)[0]
self._ta = self._ta.unstack(tensor_boards)
# Remove the number from the board when extracted
# The removal is just the set of the number to -1
# When a row or a column becomes a line of -1s then bingo!
for number in extractions:
if self._stop:
break
for idx in tf.range(tot_boards):
board = self._ta.read(idx)
board = tf.where(tf.equal(number, board), -1, board)
if self.is_winner(board):
self._stop.assign(tf.constant(True))
self._winner_board.assign(board)
self._last_number.assign(number)
break
self._ta = self._ta.write(idx, board)
return self._winner_board, self._last_number
```

Note that `write`

produces an “operation” that must be assigned to itself (`self._ta = self._ta.write(idx, board)`

).

### Execution

We have all we need to play the game and compute the final result

```
bingo = Bingo()
winner_board, last_number = bingo(extractions, boards)
_score(winner_board, last_number)
```

Here we go, part 1 solved! We are ready for part 2.

## Day 4: Giant Squid: part two

Part 2 requires to **figure out which board will win last** and compute, thus, the final score of this board.

### Design phase - part two

Finding the last winning board requires drawing all the numbers, playing the game, ignore the winning boards until we reach the end of the loop.
The idea is to remove from the game a board as soon as it becomes a winning board. In this way, we don’t place `-1`

where’s no more needed (otherwise we end up with all the boards full of `-1`

).

Removing a board from the game is trivial thanks to our representation. Whenever we find a winning board (row/column full of -1) we can just set **all the values** of this board to a number that will never be drawn: `0`

.

### Invalidating the boards

With a small modification of the `call`

method, we can solve the puzzle.

```
def __call__(
self,
extractions: tf.data.Dataset,
boards: tf.data.Dataset,
first_winner: tf.Tensor = tf.constant(True),
) -> Tuple[tf.Tensor, tf.Tensor]:
# Convert the datasaet to a tensor and assign it to the ta
# use the tensor to get its shape and know the numnber of boards
tensor_boards = tf.convert_to_tensor(list(boards)) # pun intended
tot_boards = tf.shape(tensor_boards)[0]
self._ta = self._ta.unstack(tensor_boards)
# Remove the number from the board when extracted
# The removal is just the set of the number to -1
# When a row or a column becomes a line of -1s then bingo!
for number in extractions:
if self._stop:
break
for idx in tf.range(tot_boards):
board = self._ta.read(idx)
board = tf.where(tf.equal(number, board), -1, board)
if self.is_winner(board):
self._winner_board.assign(board)
self._last_number.assign(number)
if first_winner:
self._stop.assign(tf.constant(True))
break
# When searching for the last winner
# we just invalidate every winning board
# by setting all the values to zero
board = tf.zeros_like(board)
self._ta = self._ta.write(idx, board)
return self._winner_board, self._last_number
```

We added the `first_winner`

parameter that will change the behavior of the `call`

method: when `True`

it behaves like required for part 1, when false it finds the last winning board.

### Execution - part two

Very easy

```
## --- Part Two ---
# Figure out the last board that will win
bingo = Bingo()
last_winner_board, last_number = bingo(extractions, boards, tf.constant(False))
_score(last_winner_board, last_number)
```

It works! Problem 4 is solved!

## Conclusion

You can see the complete solution in folder `4`

on the dedicated Github repository: https://github.com/galeone/tf-aoc.

We are using `tf.TensorArray`

declaring them in the `__init__`

and this leads to the very same limitations we faced while solving the day 3 puzzle.

I already solved puzzles 5 and 6 and both have been fun. In particular, while solving the day 6 puzzle I realized that the limitation I discovered only depends on where we declare and use TensorArray. My solution for that puzzle uses a `tf.TensorArray`

in a `tf.function`

-decorated function and it works pretty well! So stay tuned for that article!

The next article, however, will be about my solution to the day 5 puzzle, which has been solved without using any `tf.TensorArray`

but contains some easy but interesting mathematical concept that’s worth writing about :)

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