\section{Implementation}\label{sec:implementation}

\subsection{Jupyter}\label{subsec:jupyter}

To get accurate performance measures the active-learning process was implemented in a Jupyter notebook first.
This helps to choose which of the methods performs the best and which one to use in the final Dagster pipeline.
A straight forward machine-learning pipeline was implemented with the help of Pytorch and RESNet-18.

\begin{lstlisting}[language=Python, caption=Certainty sampling process of selected metric]
df = df.sort_values(by=['score'])

# match the currently active sampling metric
match predict_mode:
    case PredictMode.HIGHANDLOWCERTAINTY:
        train_samples = pd.concat([df[:int(batch_size/2)], df[-int((batch_size/2)+batch_size%2):]])["sample"].values.tolist()
        unlabeled_samples += df[int(batch_size/2):-int(batch_size/2)]["sample"].values.tolist()
        pass
    case PredictMode.LOWCERTAINTY:
        train_samples = df[:batch_size]["sample"].values.tolist()
        unlabeled_samples += df[batch_size:]["sample"].values.tolist()
        pass
    case PredictMode.HIGHCERTAINTY:
        train_samples = df[-batch_size:]["sample"].values.tolist()
        unlabeled_samples += df[:-batch_size]["sample"].values.tolist()
        pass
    case PredictMode.MIDCERTAINTY:
        train_samples = df[int(sample_size/2 - batch_size/2):int(sample_size/2 + batch_size/2)]["sample"].values.tolist()
        unlabeled_samples += df[:int(sample_size/2 - batch_size/2)][-int(sample_size/2 - batch_size/2):]["sample"].values.tolist()
        pass
    case PredictMode.NONE:
        train_samples = s[:batch_size]
        unlabeled_samples.extend(s[batch_size:])
        pass
\end{lstlisting}

Moreover, the Dataset was manually imported and preprocessed with random augmentations.
After each loop iteration the Area Under the Curve (AUC) was calculated over the validation set to get a performance measure.
All those AUC were visualized in a line plot, see~\ref{sec:experimental-results} for the results.

\subsection{Balanced sample selection}

To avoid the model to learn only from one class, the sample selection process was balanced as mentioned in~\ref{par:furtherimprovements}.
Simply sort by predicted class first and then select the $\mathcal{B}/2$ lowest certain samples per class.
This should help to balance the sample selection process.

\begin{lstlisting}[language=Python, caption=Certainty sampling process with class balancing]
# sort by pseudolabel
df.sort_values(by=['pseudolabel'], inplace=True)
# sort half batches by pseudolabel + score
df[:int(sample_size/2)] = df[:int(sample_size/2)].sort_values(by=['pseudolabel', 'score'])
df[int(sample_size/2):] = df[int(sample_size/2):].sort_values(by=['pseudolabel', 'score'])

halfbatchsize = int(batch_size/2)
train_samples = pd
  .concat([df[:halfbatchsize],df[int(sample_size/2):int(sample_size/2)+halfbatchsize]])["sample"]
  .values.tolist()
unlabeled_samples += pd
  .concat([df[halfbatchsize:int(sample_size/2)], df[int(sample_size/2)+halfbatchsize:]])["sample"]
  .values.tolist()

\end{lstlisting}

\subsection{Dagster with Label-Studio}\label{subsec:dagster-with-label-studio}

The main goal is to implement an active learning loop with the help of Dagster and Label-Studio.
The active learning loop was split as much as possible into assets and graph assets.
This helps building reusable building blocks and to keep the code clean.

Most of the python routines implemented in section~\ref{subsec:jupyter} were reused here and just slightly modified to fit the Dagster pipeline.

\begin{figure}
    \centering
    \includegraphics[width=\linewidth]{../rsc/dagster/assets}
    \caption{Dagster asset graph}
    \label{fig:dagster_assets}
\end{figure}

\ref{fig:dagster_assets} shows the implemented assets in which the task is split.
Whenever an asset materializes it is stored in the Dagster database.
This helps to keep track of the data and to rerun the pipeline with the same data.
\textit{train\_sup\_model} is the main asset that trains the model with the labeled samples.
\textit{inference\_unlabeled\_samples} is the asset that predicts the scores for the unlabeled samples und updates them with the Label-Studio API.
This pipeline is triggered by the machine learning backend defined within Label-Studio.
It runs every $\mathcal{B} - t$ samples, where t is a buffer to compensate the time the pipeline needs to run to avoid overlaps (e.g.\ t=10).
Such that Label-Studio has always samples with scores produced by the current model to rank the samples presented to the user.
\begin{figure}
    \centering
    \subfloat[Score prediction graph asset]{
        \includegraphics[width=0.45\linewidth]{../rsc/dagster/predict_scores}
        \label{fig:predict_scores}
    }
    \hfill
    \subfloat[Model training graph asset]{
        \includegraphics[width=0.45\linewidth]{../rsc/dagster/train_model}
    \label{fig:train_model}
    }
    \caption{Dagster graph assets}
\end{figure}

\ref{fig:train_model} shows the train model asset in detail.
It loads the data for training, trains the model and saves the model automatically due to the Dagster asset system.
Moreover, testing data is loaded and the model is evaluated with the test data to get a performance measure.
\ref{fig:predict_scores} shows the predict scores asset in detail.
It samples $\mathcal{S}$ samples from the unlabeled samples $\mathcal{X}_U$ and predicts the scores.
Then it connects to the Label-Studio API with an API key and updates the scores of the samples.
