<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Deep Learning on chriscomputing.de</title><link>https://blog.chriscomputing.de/tags/deep-learning/</link><description>Recent content in Deep Learning on chriscomputing.de</description><generator>Hugo -- gohugo.io</generator><language>en-EN</language><copyright>chriscomputing (CC BY-NC-SA 4.0)</copyright><lastBuildDate>Mon, 02 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.chriscomputing.de/tags/deep-learning/index.xml" rel="self" type="application/rss+xml"/><item><title>Multi-label classification of pallet movement using transformers</title><link>https://blog.chriscomputing.de/blog/pal2sim-classification-transformers/</link><pubDate>Mon, 02 Nov 2026 00:00:00 +0000</pubDate><guid>https://blog.chriscomputing.de/blog/pal2sim-classification-transformers/</guid><description>&lt;p&gt;A while ago, a friend and I decided to participate in the &lt;a href="https://www.pal2sim.com/?tab=competition"&gt;pal2sim&lt;/a&gt; competition. The competition was about multi-label classification of IMU data recorded on pallets inside a warehouse. It aimed at developing classifiers for logistical activities such as storage, retrieval, wrapping, and transportation &lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;. You can find the code on my &lt;a href="https://github.com/chriscomputing/pal2sim-competition"&gt;Github&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="pallet-imu-data"&gt;Pallet IMU data&lt;/h2&gt;
&lt;p&gt;The organizers provided annotated IMU timeseries data generated by an &lt;strong&gt;acceleration sensor&lt;/strong&gt; (x,y,z-axis), a &lt;strong&gt;gyroscope&lt;/strong&gt; (x,y,z-axis) and a &lt;strong&gt;barometer&lt;/strong&gt; (x-axis). The annotated classes are&lt;/p&gt;</description><content:encoded><![CDATA[<p>A while ago, a friend and I decided to participate in the <a href="https://www.pal2sim.com/?tab=competition">pal2sim</a> competition. The competition was about multi-label classification of IMU data recorded on pallets inside a warehouse. It aimed at developing classifiers for logistical activities such as storage, retrieval, wrapping, and transportation <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>. You can find the code on my <a href="https://github.com/chriscomputing/pal2sim-competition">Github</a>.</p>
<h2 id="pallet-imu-data">Pallet IMU data</h2>
<p>The organizers provided annotated IMU timeseries data generated by an <strong>acceleration sensor</strong> (x,y,z-axis), a <strong>gyroscope</strong> (x,y,z-axis) and a <strong>barometer</strong> (x-axis). The annotated classes are</p>
<ul>
<li>Driving (straight and curve)</li>
<li>Lifting (raising and lowering)</li>
<li>Standing</li>
<li>Docking</li>
<li>Forks (from the front or side)</li>
<li>Wrapping</li>
<li>Wrapping preparation</li>
</ul>
<p>In the provided preprocessing, wrapping preparation, docking, forks and standing were all mapped to &ldquo;Stationary processes&rdquo;.</p>
<h3 id="train-validation-and-test-splitting">Train, Validation and Test splitting</h3>
<p>The data is segmented into 4 folds of roughly equal size. We used two folds for training, one for validation and one for testing. CNN and Transformer used the validation split during training for early stopping, the Random Forest used the validation split for training as well, which was explicitly allowed by the task organizers. Since the Random Forest does not have epochs, it does not need the validation split for early stopping.</p>
<h3 id="dataset-investigation">Dataset investigation</h3>
<p>In all folds, stationary processes are overrepresented, while lifting (both raising and lowering) is severely underrepresented. Training the underrepresented classes turned out to be our largest problem.</p>
<p>
















  
  
  
      
      
  <figure class="figure " >
  <picture  >
  <img class="figure-img img-fluid" src="https://blog.chriscomputing.de/images/posts/pal2sim/f1_class-distribution.png?width=600&v=f263e8d5151879ff92ea5be1a2294b4c" alt="Class distribution of one fold. Datapoints with stationary process label are more than double as frequent as the next most frequent class." title="Class distribution of one fold. Datapoints with stationary process label are more than double as frequent as the next most frequent class." loading="lazy" height="600" width="1000" style="width: 600px" />
</picture>

  <figcaption class="figure-caption">Class distribution of one fold. Datapoints with stationary process label are more than double as frequent as the next most frequent class.</figcaption>
</figure>
</p>
<p>While we were able to learn the other classes well, classifying lifting correctly remained a problem. A closer investigation into how each feature behaves during these operations did also not reveal a lot of useful insight.</p>
<p>
















  
  
  
      
      
  <figure class="figure " >
  <picture  >
  <img class="figure-img img-fluid" src="https://blog.chriscomputing.de/images/posts/pal2sim/lifting_sensor_pairs.png?width=650&v=f263e8d5151879ff92ea5be1a2294b4c" alt="Lifting classes in Sensor Pair Space." title="Lifting classes in Sensor Pair Space" loading="lazy" height="1530" width="3240" style="width: 650px" />
</picture>

  <figcaption class="figure-caption">Lifting classes in Sensor Pair Space</figcaption>
</figure>
</p>
<h3 id="dataset-preprocessing">Dataset preprocessing</h3>
<p>Preprocessing was partially provided by the task organizers and partially added by us. It consists of cleaning the data and turning the input stream into fixed-length 3-dimensional windows, together with a downsampling to 80 Hz. Each window has a length of 12, meaning the models receive 137.5 ms of historic data and have to classify the current action based of that. This is only a snapshot of what the pallet is experiencing. This way, we rarely have to worry about a window overlapping with another action which could cause wrong classification. But, in all honesty, if we would have had more compute, we would likely have used a higher resolution and longer windows.</p>
<p>Engineered features are also available, but we found that they did not improve model capabilities, so we left them out. The provided code likewise performed scaling during preprocessing, but we moved that to each model implementation to enable per-model control.</p>
<h2 id="evaluation-of-model-quality">Evaluation of model quality</h2>
<p>The evaluation metric MCC (Matthews correlation coefficient) was given by the organizers. MCC remains a robust measure even when classes are wildly off-balance, as is the case with the given dataset. MCC returns a value between -1 and +1. A coefficient of +1 represents a perfect prediction, 0 an average random prediction and -1 an inverse prediction.</p>
<p>The standard multiclass MCC is defined as:</p>
$$
MCC = \frac{c \times s - \sum_{k=1}^{K} p_k \times t_k}{\sqrt{(s^2 - \sum_{k=1}^{K} p_k^2) \times (s^2 - \sum_{k=1}^{K} t_k^2)}}
$$<p>where</p>
<ul>
<li>$t_k = \sum_{i=1}^{K} C_{ik}$ the number of times class $k$ truly occurred</li>
<li>$p_k = \sum_{i=1}^{K} C_{ki}$ the number of times class $k$ was predicted</li>
<li>$c = \sum_{k=1}^{K} C_{kk}$ the total number of samples correctly predicted</li>
<li>$s = \sum_{i=1}^{K} \sum_{j=1}^{K} C_{ij}$ the total number of samples</li>
</ul>
<p>Above formulas are from the Scikit-learn user guide <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</p>
<p>Since multiple labels can be true at the same time (e.g. lifting the forks while driving), the organizers implemented an unweighted average over all classes. As a consequence, a particular bad class can reduce the overall score, even if it does not occur often in the data. Both lifting classes dragged down our MCC considerably.</p>
<h2 id="testing-different-models">Testing different models</h2>
<p>We tried different ML and DL models for classification, ranging from simple random forests to transformers.</p>
<h3 id="random-forest-classifier">Random forest classifier</h3>
<p>The first classifier we tried was the <code>RandomForestClassifier</code> from <code>sklearn</code> using 50 estimators and 10,000 samples. Since the original data is timeseries data, we performed feature-engineering for the random forest. For each original feature (described above), we calculated the mean, standard deviation, maximum and minimum across a short time window. This flattened the original 3D data into 2 dimensions. The model had to predict the label of this time window using only the 27 engineered features.</p>
<p>We used this classifier as our baseline for future explorations. Nevertheless, problems surfaced by this model were indicative of our problems with more advanced models. More on the results in a later section.</p>
<h3 id="cnn-classifier">CNN classifier</h3>
<p>Our second and most realistic approach was a small CNN. The architecture is fairly standard: 3 convolution layers each followed by a normalization, ReLu and dropout layer. This section is followed by a dense $64 \cdot 64$ linear head with ReLu and dropout layers again. Finally, the CNN produces 6 output logits through a linear layer, one for each class. We considered a custom implementation for asymmetric focal loss (AFL) because the dataset is highly imbalanced. Ablation showed that the simpler BCE loss works just as well, so we switched to it as it requires less parameters and is in off-the-shelf solution.</p>
<p>During training, we use the AdamW optimizer. We allowed up to 50 epochs, but implemented early stopping with a patience of 8 based on the MCC on the validation dataset. Basing it of the MCC put the early stopping metric more in line with the final competition metric. The best checkpoint is restored. In the best run, the best checkpoint was reached in epoch 29.</p>
<p>
















  
  
  
      
      
  <figure class="figure " >
  <picture  >
  <img class="figure-img img-fluid" src="https://blog.chriscomputing.de/images/posts/pal2sim/cnn1d_mccs.png?width=650&v=f263e8d5151879ff92ea5be1a2294b4c" alt="Mean per-class MCC of the best performing CNN before applying threshold optimization for training and validation." title="Mean per-class MCC of the best performing CNN before applying threshold optimization for training and validation." loading="lazy" height="750" width="1200" style="width: 650px" />
</picture>

  <figcaption class="figure-caption">Mean per-class MCC of the best performing CNN before applying threshold optimization for training and validation.</figcaption>
</figure>
</p>
<p>After training, we perform per-class threshold optimization on the validation set. For every class, a specific cut-off maximizes the correlation between prediction and ground truth. The cut-off is not only different per class, but also different per model. We ran the model over validation data and collected the resulting probabilities. The candidate grid for thresholds is between 0.05 and 0.95 in steps of 0.01, so in total 91 possible thresholds for each class. We find the best threshold per class by calculating the MCC for each possible combination of class and threshold. Contrary to expectations, this optimization step has next to no impact for stronger models. The best CNN only gained 0.013 (MCC) through this step, while worse performing models gained up to 0.107. Since we developed this step while our CNNs were performing worse, we simply kept it during later runs, although it provided next to no benefit anymore.</p>
<h3 id="a-transformer-for-classification">A transformer for classification</h3>
<p>Since I was learning about and using transformers in university at the time, I decided to try my luck with a transformer for classification. I knew that it would be absolutely overkill and that there are better models out there for this job, but I decided it would be a fun learning experience. After all, we had timeseries data, and transformers are built for sequences.</p>
<p>The transformer did not require engineered features, it only got the 7 original features. Due to us being compute restrained and transformers requiring a lot of compute, we tried different combinations of sequence length and sampling rate. We sampled down by only using every n-th measurement in the final input. Each timestep is encoded into one token, giving us 12 tokens per input. Attention mechanisms become largely trivial in our case, as all tokens are roughly equally important. In line with standard transformers for classification, e.g. BART, we used learned absolute positional encodings. For us, it has the advantage that positional embeddings turn into a lookup-table during inference, which is fast and simple. In practice, absolute embeddings are used, because they allow the model to learn a clear, well defined map of the sequence. The major trade-off is that our model can not deal with sequences longer than 12 tokens, which we find completely acceptable for our use-case.<br>
We used 3 <code>TransformerEncoderLayer</code> (128 embedding dimensions, 8 attention heads, FFN width 512). No attention mask is applied, as we use a fixed size of input tokens, and every timestep can attend to every other. The encoder output is mean-pooled over time, followed by a single linear layer producing 6 independent logits for final classification. In total, our transformer has around 0.62M parameters. Training was performed across 20 epochs with early stopping (patience 5, using MCC on validation split as criterion), again with weighted BCE for optimization with a max <code>pos_weight</code> of 5.</p>
<p>Contrary to the CNN, we did not use post-training threshold tuning. As discussed above, this step proved unnecessary once the CNN improved, so we saw no reason to implement it in the transformer, which did perform better than the CNN.</p>
<h4 id="using-domain-knowledge-to-improve-transformer-performance">Using domain knowledge to improve transformer performance</h4>
<p>Often enough, the transformer was unable to distinguish between classes which can not be true simultaneously (i.e. driving straight and through a curve or raising and lowering at the same time). It can do so, because it treats every output class a independent Bernoulli decisions. To combat this problem, we used domain knowledge to post-process the logits. We manually created as list of impossible combinations. If they occur, the pipeline returns only the class with the highest probability. However, there are legal combinations of labels, for example one form of driving with one form of lifting. While keeping all single actions, we compute the log-probability of every legal combination derived from the training data and pick the best-scoring out of the single actions or legal combinations using the maximum-likelihood.</p>
<h2 id="results">Results</h2>
<table>
	<thead>
			<tr>
					<th>Model</th>
					<th>Training MCC</th>
					<th>Validation MCC</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Random forest</td>
					<td>0.645</td>
					<td>0.388</td>
			</tr>
			<tr>
					<td>CNN</td>
					<td>0.798</td>
					<td>0.711</td>
			</tr>
			<tr>
					<td>Transformer</td>
					<td>0.915</td>
					<td>0.735</td>
			</tr>
			<tr>
					<td><strong>Competition winner</strong></td>
					<td>-</td>
					<td><strong>0.88</strong></td>
			</tr>
	</tbody>
</table>
<p>Above table compares the best performance of our models with the model which would eventually go on to win the competition <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>.</p>
<p>As the final results show, the transformer barely managed to beat the much simpler CNN. As alluded to earlier, we knew a transformer would not be the ideal model. Additionally, we probably made some mistakes along the way. Given all of that, I am surprised the transformer performed as good as it did.</p>
<p>We mainly struggled with the lifting classes, as they are severely underrepresented in the training data and not easy to distinguish from other classes. Per-class diagnostics make this obvious (table showing diagnostics for best model each):</p>
<table>
	<thead>
			<tr>
					<th>Class</th>
					<th style="text-align: right">CNN MCC</th>
					<th style="text-align: right">CNN Recall</th>
					<th style="text-align: right">CNN Precision</th>
					<th style="text-align: right">Transformer MCC</th>
					<th style="text-align: right">Transformer Recall</th>
					<th style="text-align: right">Transformer Precision</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Turntable wrapping</td>
					<td style="text-align: right">0.993</td>
					<td style="text-align: right">0.992</td>
					<td style="text-align: right">0.996</td>
					<td style="text-align: right">0.983</td>
					<td style="text-align: right">0.997</td>
					<td style="text-align: right">0.974</td>
			</tr>
			<tr>
					<td>Stationary processes</td>
					<td style="text-align: right">0.947</td>
					<td style="text-align: right">0.958</td>
					<td style="text-align: right">0.991</td>
					<td style="text-align: right">0.942</td>
					<td style="text-align: right">0.957</td>
					<td style="text-align: right">0.987</td>
			</tr>
			<tr>
					<td>Driving (curve)</td>
					<td style="text-align: right">0.861</td>
					<td style="text-align: right">0.870</td>
					<td style="text-align: right">0.895</td>
					<td style="text-align: right">0.801</td>
					<td style="text-align: right">0.892</td>
					<td style="text-align: right">0.780</td>
			</tr>
			<tr>
					<td>Driving (straight)</td>
					<td style="text-align: right">0.712</td>
					<td style="text-align: right">0.897</td>
					<td style="text-align: right">0.650</td>
					<td style="text-align: right">0.713</td>
					<td style="text-align: right">0.877</td>
					<td style="text-align: right">0.663</td>
			</tr>
			<tr>
					<td>Lifting (raising)</td>
					<td style="text-align: right">0.360</td>
					<td style="text-align: right">0.267</td>
					<td style="text-align: right">0.519</td>
					<td style="text-align: right">0.520</td>
					<td style="text-align: right">0.585</td>
					<td style="text-align: right">0.487</td>
			</tr>
			<tr>
					<td>Lifting (lowering)</td>
					<td style="text-align: right">0.393</td>
					<td style="text-align: right">0.639</td>
					<td style="text-align: right">0.285</td>
					<td style="text-align: right">0.452</td>
					<td style="text-align: right">0.639</td>
					<td style="text-align: right">0.360</td>
			</tr>
			<tr>
					<td><strong>Mean (official)</strong></td>
					<td style="text-align: right"><strong>0.711</strong></td>
					<td style="text-align: right">–</td>
					<td style="text-align: right">–</td>
					<td style="text-align: right"><strong>0.735</strong></td>
					<td style="text-align: right">–</td>
					<td style="text-align: right">–</td>
			</tr>
	</tbody>
</table>
<p>Due to its unique characteristics, turntable wrapping is easy to distinguish. Stationary processes are both relatively easy and have a lot of training data, which enable high scores for this class as well. Driving and lifting are more problematic. Since the evaluation metric is unweighted average MCC, especially the lifting classes with their bad scores and low support decreased the result considerably. The CNN actually tends to outperform the transformer on easier classes. The transformer wins essentially only on lifting, mainly because it is better in recalling <code>Lifting (raising)</code>, without losing much precision.</p>
<h2 id="personal-take-aways">Personal take-aways</h2>
<p>For one evening, I think we produced a fairly good model. I also learned a thing or two about analyzing and optimizing models. I especially enjoyed having the opportunity to experiment with a transformer, even though it is really small. I would love to work more with them on a larger scale. Given the lack of lifting data, I did consider experimenting with synthetic data generation, but was unsure if it was allowed by the competition rules and did not have time to dive deep, so ultimately I did not attempt it. This one is still on my bucket list.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://www.pal2sim.com/?tab=about">Pal2Sim about section</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://scikit-learn.org/stable/modules/model_evaluation.html#matthews-corrcoef">Scikit-learn user guide</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://www.pal2sim.com/?tab=competition">Pal2Sim competition</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item></channel></rss>