<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Python on Methods Bites</title>
    <link>https://socialsciencedatalab.mzes.uni-mannheim.de/tags/python/</link>
    <description>Recent content in Python on Methods Bites</description>
    <generator>Hugo -- gohugo.io</generator>
    <lastBuildDate>Fri, 10 Jan 2025 02:00:00 +0100</lastBuildDate>
    
        <atom:link href="https://socialsciencedatalab.mzes.uni-mannheim.de/tags/python/index.xml" rel="self" type="application/rss+xml" />
    
    
    <item>
      <title>Computationally Analyzing Politicians’ Body Language Using Pose Estimation</title>
      <link>https://socialsciencedatalab.mzes.uni-mannheim.de/article/computationally-pose-estimation/</link>
      <pubDate>Fri, 10 Jan 2025 02:00:00 +0100</pubDate>
      
      <guid>https://socialsciencedatalab.mzes.uni-mannheim.de/article/computationally-pose-estimation/</guid>
      <description><![CDATA[
        


<!-- 
Please specify all required packages in the pkgs vector
Note: Please assign a name (such as "packages") to each code chunk 
-->
<!-- 
One introductory paragraph for the blog post: 
Topic, presenter, workshop, relevance 
-->
<p>Politicians can increase the appeal of their speeches through nonverbal cues such as gestures and vocal emphasis. Understanding the factors that make political speech appealing is central to political science research, yet studying nonverbal cues during political speech is difficult due to their audiovisual nature. Pose estimation models—a class of computer vision models that locate and trace human body key points, such as hands, ellbows, and shoulders throughout videos—offer a valuable opportunity to computationally assess politicians’ body language in video recordings. In this <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/categories/tutorials/">Methods Bites Tutorial</a>, <a href="https://oliver-rittmann.com">Oliver Rittmann</a> offers a hands-on recap of his workshop “Computationally Analyzing Politicians’ Body Language Using Pose Estimation” in the <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/page/events/">MZES Social Science Data Lab</a> in the Fall Term 2024, in which he introduces pose estimation models and explains their implementation for analysing the body language of speakers.</p>
<!-- Optional: One paragraph on learning objectives -->
<p>After reading this blog post and engaging with the applied exercises, readers should be able to:</p>
<ul>
<li>implement pose estimation models in Python to analyse video recordings of speeches</li>
<li>load the pose estimation output into R</li>
<li>organize the pose estimation data in a time-series data set</li>
<li>visualize the pose estimation data</li>
<li>calculate a measure of gesticulation and posture</li>
</ul>
<!-- If applicable: Note with references on related materials -->
<p><em>Note:</em> This blog post provides a summary of Oliver Rittmann’s workshop in the <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/page/events/">MZES Social Science Data Lab</a>. The original workshop materials, including slides and scripts, are available on <a href="https://github.com/orittmann/ssdl_body_language">GitHub</a>.
A live recording of the workshop is available on our <a href="https://youtu.be/w6orbDvQSN4">YouTube Channel</a>.</p>
<!-- 
Generate an overview of the article 
Note: Section anchors are generated automatically from section headings as 
      hyphenated lower-case labels without special characters; e.g. a section
      title "Why R?" will result in the anchor "#why-r".
-->
<div id="overview" class="section level3">
<h3>Overview</h3>
<ol style="list-style-type: decimal">
<li><a href="#what-are-pose-estimation-models-and-why-are-they-helpful-for-analyzing-body-language"><strong>What Are Pose Estimation Models and Why Are They Helpful For Analyzing Body Language?</strong></a></li>
<li><a href="#implementing-pose-estimation"><strong>Impelementing Pose Estimation</strong></a></li>
<li><a href="#loading-pose-estimation-movenet-output-into-r"><strong>Loading Pose Estimation Output into R</strong></a></li>
<li><a href="#organizing-pose-estimation-data-as-a-time-series-data-set"><strong>Organizing Pose Estimation Output in a Time-Series Data Set</strong></a></li>
<li><a href="#visualizing-pose-estimation-data"><strong>Visualizing Pose Estimation Data</strong></a></li>
<li><a href="#gesticulation"><strong>Calculating Measure of Gesticulation</strong></a></li>
<li><a href="#posture"><strong>Calculating Measure of Posture</strong></a></li>
<li><a href="#wrapping-up-and-further-readings"><strong>Wrapping Up</strong></a></li>
</ol>
</div>
<div id="what-are-pose-estimation-models-and-why-are-they-helpful-for-analyzing-body-language" class="section level3">
<h3>What Are Pose Estimation Models and Why Are They Helpful For Analyzing Body Language?</h3>
<p>How voters perceive a political speech does not only depend on the textual content of a speech, but also on the speakers’ nonverbal behavior. Two speeches with identical text can produce dramatically different effects on listeners based solely on their nonverbal delivery. Thus, to gain a comprehensive picture of political speeches, we should take its nonverbal elements into account. This blog post focuses on one aspect of nonverbal communication: body language.</p>
<p>Politicians’ body language during political speeches is encoded in video recordings. This poses a problem if we want to analyse body language on a large scale: As political scientists, we are not used to working with videos as a data source. If we think about videos as data, we are confronted with large amounts of highly unstructured data. Videos are series of image frames, which itself consist of pixels. Individual pixels have no informational value and we cannot use the data analysis tools we are familiar with to analyse them. If we want to computationally analyze video recordings, our challenge is to extract the information that we are interested in from the videos and organize it in a data format that we can actually work with.</p>
<p>Pose estimation models, a class of computer vision models that locate and trace key points of the human body throughout videos, offer a helpful way to analyze politicians’ body language during speeches. The following figure illustrates this based on four frames of a speech in the German Bundestag.</p>
<div class="figure" style="text-align: center"><span style="display:block;" id="fig:pic-movenet"></span>
<img src="../../../../../../../../article/computationally-pose-estimation/figures/movenet_demo_Brehm.png" alt="Upper panel shows four single video frames, while the lower panel depicts the corresponding body keypoints." width="50%" />
<p class="caption">
Figure 1: Upper panel shows four single video frames, while the lower panel depicts the corresponding body keypoints.
</p>
</div>
<p>As you can see, pose estimation essentially extracts the information that we are interested in—information on the speaker’s body language—from the frames while discarding all other information that is less interesting for this task.</p>
<p>When we apply pose estimation to an entire video, we’ll get time series of all detected keypoints. These time series contain information about the movement of those key points, for example the movement of the speaker’s hands. The key idea is that we can use this data to quantify such movement. Such quantifications can provide us with measures of particular aspects of body language. In this tutorial, we’ll use pose estimation to construct measures of gesticulation and posture. Gesticulation, which I define as dynamic use of gestures, specifically through hand movement, will be operationalized as the average between-frame movement of the speaker’s wrists. This works because higher distances of the wrist location between frames tell us that the speaker moves their hand. Posture, which I define as the adoption of body size increasing postures, will be operationalized as the average height of the speaker’s (higher) wrist, relative to their shoulder. The following figure illustrates both measures:
<img src="../../../../../../../../article/computationally-pose-estimation/figures/wrist_movement_height_demo.png" width="50%" style="display: block; margin: auto;" /></p>
<p>I developed these measures in a working paper, which is linked at the end of this tutorial. If the introduction of the measures here was too brief, I invite you to take a closer look into that paper for a more detailed exposition.</p>
<p>In the following sections, we will apply pose estimation to two short video sequences using Python. We’ll then load the resulting data into R and explore it. We’ll visualize the data to get a feeling for it, and ultimately calculate the measures of gesticulation and posture.</p>
</div>
<div id="implementing-pose-estimation" class="section level3">
<h3>Implementing Pose Estimation</h3>
<!-- Add some text here  -->
<p>We start by applying Tensorflow’s pose estimation model <a href="https://www.tensorflow.org/hub/tutorials/movenet">MoveNet Thunder</a> to two short videos of two speeches in the German Bundestag. If you would like to replicate the analysis on your computer, you can find the videos of those speeches in the <a href="https://github.com/orittmann/ssdl_body_language.git">GitHub repository</a> accompanying this tutorial. You can also implement this section online on <a href="https://colab.research.google.com/github/orittmann/ssdl_body_language">Google Colab</a>.</p>
<p>The two videos that we would like to analyze are located at</p>
<ul>
<li><code>ssdl_body_language/videos/speech1_gabriela_heinrichs.mp4</code></li>
<li><code>ssdl_body_language/videos/speech2_klaus_ernst.mp4</code>.</li>
</ul>
<p>The repository also includes the the pre-trained pose estimation model that we will use to analyze the videos:</p>
<ul>
<li><code>ssdl_body_language/model/lite-model_movenet_singlepose_thunder_3.tflite</code>.</li>
</ul>
<p>You can find more details on MoveNet <a href="https://www.tensorflow.org/hub/tutorials/movenet">here</a>.</p>
<p>I’ve chosen two speeches that differ strongly regarding the speakers’ nonverbal displays. <strong>Speech 1</strong> by Gabriela Heinrich can be characterized as a speech with low nonverbal effort, while <strong>Speech 2</strong> by Klaus Ernst is a speech with high nonverbal effort. Our goal is to see how we can quantify these contrastive delivery styles using pose estimation. You can see both speeches below:</p>
<div class="figure" style="text-align: center"><span style="display:block;" id="fig:pic-gif-1"></span>
<img src="../../../../../../../../article/computationally-pose-estimation/figures/speech1_gabriela_heinrich.gif" alt="Speech 1: Garbiela Heinrich, low nonverbal effort" width="50%" />
<p class="caption">
Figure 2: Speech 1: Garbiela Heinrich, low nonverbal effort
</p>
</div>
<div class="figure" style="text-align: center"><span style="display:block;" id="fig:pic-gif-2"></span>
<img src="../../../../../../../../article/computationally-pose-estimation/figures/speech2_klaus_ernst.gif" alt="Speech 2: Klaus Ernst, high nonverbal effort" width="50%" />
<p class="caption">
Figure 3: Speech 2: Klaus Ernst, high nonverbal effort
</p>
</div>
<p>Before we implement the model, we need to load all necessary dependencies in Python:</p>
<pre class="python"><code># import dependencies
import tensorflow as tf
import numpy as np
from matplotlib import pyplot as plt
import cv2

# to store resulting data as json
import json

# to get filenames in directory
import os
import fnmatch</code></pre>
<p>..and we need to load and prepare the pose estimation model:</p>
<pre class="python"><code>interpreter = tf.lite.Interpreter(model_path=&#39;ssdl_body_language/model/lite-model_movenet_singlepose_thunder_3.tflite&#39;)
interpreter.allocate_tensors()</code></pre>
<p>Next, we set up the function <code>make_keypoint_detection()</code>. This function processes an input video frame-by-frame and performs pose estimation detection using TensorFlow’s MoveNet model, which we loaded in the previous step. In particular, the function resizes each frame of the video to the required size, prepares it for the model, runs inference to get key points, stores the key points, and finally returns all the key points detected from the video.</p>
<pre class="python"><code>def make_keypoint_detection(video_path):

    # &quot;output_images&quot; is an empty list that will store the keypoints detected
    # in each frame of the video.
    output_images = []

    # &quot;cap&quot; is a video capturing object that allows us to read the frames of
    # the video stored at video_path
    cap = cv2.VideoCapture(video_path)

    # We initialize a while-loop that that loops through all frames of the video
    # The loop is open as long as the video capture object &quot;cap&quot; is open.
    # That is, until all frames are analyzed.
    while cap.isOpened():

        # We read the current frame of the video
        # &quot;ret&quot; is boolean, indicating if the frame was read successfully
        # &quot;frame&quot; is the actual frame of the video
        ret, frame = cap.read()

        # We only proceed if the frame is read correctly (i.e., if ret is TRUE)
        if not ret:
            print(&quot;Stream end.&quot;)
            break

        # Image Preparation:
        # We create a copy of the current frame to avoid modyfing the original.
        img = frame.copy()

        # MoveNet Thunder requires a frame size of 256x256
        # For that reason, we resize the frame.
        # This includes padding since the original video is not square
        img = tf.image.resize_with_pad(np.expand_dims(img, axis=0), 256, 256)

        # We convert the resized image to a TensorFlow float32 tensor,
        # so that we can feed it into the model
        input_image = tf.cast(img, dtype=tf.float32)

        # Setting Up Model Input and Output:
        input_details = interpreter.get_input_details()
        output_details = interpreter.get_output_details()

        # Running inference:
        # - We set up the input tensor with the prepare input frame
        # - We run the model
        # - We retrieve the output tensor, which contains the keypoints
        interpreter.set_tensor(input_details[0][&#39;index&#39;], input_image.numpy())
        interpreter.invoke()
        keypoints_with_scores = interpreter.get_tensor(output_details[0][&#39;index&#39;])

        # Storing the Results:
        # We transform numpy array to a list (this makes it easier to store as
        # the output as a .json file later) and append it to &quot;output_images&quot;
        # for storage
        output_images.append(keypoints_with_scores.tolist())

    # Final Steps:
    # - We release the video capturing object
    # - We return the list of keypoints detected in each frame of the video
    cap.release()

    return output_images</code></pre>
<p>Although we apply this function to only the two videos you saw above, we’ll nevertheless write the code in a way that allows us to efficiently apply it to as many videos as we want. To do so, we create a vector, <code>video_files</code>, that contains the filenames of all videos we would like to analyze.</p>
<pre class="python"><code>video_files = fnmatch.filter(os.listdir(&quot;ssdl_body_language/videos&quot;), &quot;*.mp4&quot;)

print(video_files)</code></pre>
<p>…and loop through this vector, applying our function to each video in <code>video_files</code>.</p>
<pre class="python"><code># loop over all videos
for i in np.arange(0, len(video_files)):
    # We start by retrieving the file path to the current video
    current_file = video_files[i]
    current_path = &quot;ssdl_body_language/videos/&quot; + current_file

    # We print a message indicating the start of inference for the current video
    print(&quot;Start inference for video &quot; + str(i) + &quot;: &quot; + current_file)

    # Executing keypoint detection:
    # We call the &quot;make_keypoint_detection&quot; function with the path to the
    # current video and store the resulting keypoints in &quot;keypoints_result_tmp&quot;
    keypoints_result_tmp = make_keypoint_detection(current_path)

    # Store data:
    # We specify the filepath and file name of the output file
    # and store the output
    res_json_file = current_file.replace(&quot;mp4&quot;, &quot;json&quot;)
    res_json_file_path = &quot;ssdl_body_language/movenet_results/&quot; + res_json_file

    with open(res_json_file_path, &#39;w&#39;) as fp:
        json.dump(keypoints_result_tmp, fp)

    # We delete the temporary keypoint results to free up memory
    del keypoints_result_tmp

    # Finally, we print a message indicating the end of inference for the
    # current video
    print(&quot;End inference for video &quot; + str(i) + &quot;: &quot; + current_file)
</code></pre>
<p>That’s it. We now applied the pose estimation model to both videos and stored the results as <code>.json</code> files in <code>ssdl_body_language/movenet_results</code>. From here on, we will continue working with R.</p>
</div>
<div id="loading-pose-estimation-movenet-output-into-r" class="section level3">
<h3>Loading pose estimation (MoveNet) output into R</h3>
<!-- Add some text here  -->
<p>Having applied pose estimation to the two videos and stored the output on our computer, we’ll now analyze the output data using <code>R</code>. The pose estimation data comes in <code>.json</code> format and is stored in the folder <code>movenet_results</code>. We have two files, one for each video. Let’s load the data into our environment to see what it looks like.</p>
<pre class="r"><code>speech1_raw &lt;- jsonlite::fromJSON(readLines(&quot;./computationally-pose-estimation/movenet_results/speech1_gabriela_heinrich.json&quot;))</code></pre>
<pre><code>## Warning in
## readLines(&quot;./computationally-pose-estimation/movenet_results/speech1_gabriela_heinrich.json&quot;):
## incomplete final line found on
## &#39;./computationally-pose-estimation/movenet_results/speech1_gabriela_heinrich.json&#39;</code></pre>
<pre class="r"><code>speech2_raw &lt;- jsonlite::fromJSON(readLines(&quot;./computationally-pose-estimation/movenet_results/speech2_klaus_ernst.json&quot;))</code></pre>
<pre><code>## Warning in
## readLines(&quot;./computationally-pose-estimation/movenet_results/speech2_klaus_ernst.json&quot;):
## incomplete final line found on
## &#39;./computationally-pose-estimation/movenet_results/speech2_klaus_ernst.json&#39;</code></pre>
<pre class="r"><code>dim(speech1_raw)</code></pre>
<pre><code>## [1] 274   1   1  17   3</code></pre>
<p>The dimensions of the first speech object are 274 x 1 x 1 x 17 x 3. The first dimension reflects the number of frames of the video. Let’s have a look at the data of one individual frame:</p>
<pre class="r"><code># the first frame
speech1_raw[1,,,,]</code></pre>
<pre><code>##            [,1]      [,2]       [,3]
##  [1,] 0.3828655 0.5242524 0.68463719
##  [2,] 0.3538715 0.5414523 0.75235844
##  [3,] 0.3582214 0.5031287 0.48237407
##  [4,] 0.3297369 0.5690745 0.69024754
##  [5,] 0.3400428 0.4759488 0.75820732
##  [6,] 0.3940416 0.6355466 0.90980828
##  [7,] 0.4116655 0.4349169 0.86186630
##  [8,] 0.5216534 0.6782555 0.78337336
##  [9,] 0.5355401 0.3803662 0.79445451
## [10,] 0.6526011 0.6498522 0.74373788
## [11,] 0.6306298 0.3373373 0.71716160
## [12,] 0.6974007 0.6186860 0.67185098
## [13,] 0.6894653 0.4781892 0.77422142
## [14,] 0.5340295 0.6774333 0.02469357
## [15,] 0.7561160 0.4362402 0.02170779
## [16,] 0.7344654 0.5591633 0.01254712
## [17,] 0.7249266 0.4822194 0.01522222</code></pre>
<p>The first two columns represent the y- and x-coordinates of the 17 body key points on a scale between 0 and 1:</p>
<ul>
<li>1 nose</li>
<li>2 left eye</li>
<li>3 right eye</li>
<li>4 left ear</li>
<li>5 right ear</li>
<li>6 left shoulder</li>
<li>7 right shoulder</li>
<li>8 left elbow</li>
<li>9 right elbow</li>
<li>10 left wrist</li>
<li>11 right wrist</li>
<li>12 left hip</li>
<li>13 right hip</li>
<li>14 left knee</li>
<li>15 right knee</li>
<li>16 left ankle</li>
<li>17 right ankle</li>
</ul>
<p>The third column tells us how confident the model was in its detection, with confidence scores ranging between 0 and 1.</p>
</div>
<div id="organizing-pose-estimation-data-as-a-time-series-data-set" class="section level3">
<h3>Organizing pose estimation data as a time-series data set</h3>
<!-- Add some text here  -->
<p>The current format of the data is not very convenient and it makes sense to bring it into a format easier to work with. The following function requires the keypoint data loaded in the previous step, a file name, and the length of the respective video. The function takes our keypoint data as input and puts it into a time-series data frame that we can work with more easily.</p>
<pre class="r"><code>array_to_timeseries &lt;- function(data_list, 
                                file_name,
                                video_length){
  ts_dat &lt;- 
    data.frame(file_name = rep(file_name, dim(data_list)[1]),
               frame = 1:dim(data_list)[1],
               timestamp = NA,
               matrix(NA, ncol = 17, nrow = dim(data_list)[1]),
               matrix(NA, ncol = 17, nrow = dim(data_list)[1]),
               matrix(NA, ncol = 17, nrow = dim(data_list)[1]))
  
  # all x-coordinates will be stored in the columns 4-20
  keypoints_x_cols &lt;- 4:20 
  # all y-coordinates will be stored in the columns 21-37
  keypoints_y_cols &lt;- 21:37 
  # all confidence scores will be stored in the columns 38-54
  keypoints_confidence_cols &lt;- 38:54
  
  # assign variable names
  names(ts_dat)[keypoints_x_cols] &lt;- paste0(&quot;kp&quot;, 1:17, &quot;_x&quot;)
  names(ts_dat)[keypoints_y_cols] &lt;- paste0(&quot;kp&quot;, 1:17, &quot;_y&quot;)
  names(ts_dat)[keypoints_confidence_cols] &lt;- paste0(&quot;kp&quot;, 1:17, &quot;_conf&quot;)
  
  # calculate timestamps
  framerate &lt;- dim(data_list)[1] / video_length
  ts_dat$timestamp &lt;- ts_dat$frame / framerate
  
  for (i in 1:dim(data_list)[1]) {
    # x-values
    ts_dat[i,keypoints_x_cols] &lt;- data_list[i,,,,2]
    # y-values
    ts_dat[i,keypoints_y_cols] &lt;- data_list[i,,,,1]
    # confidence values
    ts_dat[i,keypoints_confidence_cols] &lt;- data_list[i,,,,3]
  }
  
  return(ts_dat)
}</code></pre>
<p>Let’s apply the function to our two speeches.</p>
<pre class="r"><code>speech1_keypoints &lt;- 
  array_to_timeseries(data_list = speech1_raw,
                      file_name = &quot;speech1_gabriela_heinrich&quot;,
                      video_length = 1/25 * dim(speech1_raw)[1]) # framerate = 25fps

speech2_keypoints &lt;- 
  array_to_timeseries(data_list = speech2_raw,
                      file_name = &quot;speech2_klaus_ernst&quot;,
                      video_length = 1/25 * dim(speech2_raw)[1])</code></pre>
<p>What do we have now?</p>
<pre class="r"><code>head(speech1_keypoints)</code></pre>
<pre><code>##                   file_name frame timestamp     kp1_x     kp2_x     kp3_x
## 1 speech1_gabriela_heinrich     1      0.04 0.5242524 0.5414523 0.5031287
## 2 speech1_gabriela_heinrich     2      0.08 0.5251055 0.5423815 0.5044520
## 3 speech1_gabriela_heinrich     3      0.12 0.5248442 0.5421506 0.5037181
## 4 speech1_gabriela_heinrich     4      0.16 0.5242078 0.5404651 0.5030183
## 5 speech1_gabriela_heinrich     5      0.20 0.5209133 0.5377335 0.4998379
## 6 speech1_gabriela_heinrich     6      0.24 0.5229780 0.5378245 0.5013885
##       kp4_x     kp5_x     kp6_x     kp7_x     kp8_x     kp9_x    kp10_x
## 1 0.5690745 0.4759488 0.6355466 0.4349169 0.6782555 0.3803662 0.6498522
## 2 0.5683912 0.4757831 0.6335180 0.4332503 0.6780418 0.3806624 0.6426720
## 3 0.5665054 0.4729802 0.6320996 0.4305547 0.6771996 0.3776755 0.6494720
## 4 0.5654123 0.4712301 0.6307175 0.4294078 0.6731661 0.3774658 0.6461877
## 5 0.5627532 0.4689308 0.6294175 0.4277581 0.6723908 0.3772046 0.6492292
## 6 0.5613532 0.4663894 0.6274553 0.4237787 0.6678612 0.3776814 0.6437172
##      kp11_x    kp12_x    kp13_x    kp14_x    kp15_x    kp16_x    kp17_x
## 1 0.3373373 0.6186860 0.4781892 0.6774333 0.4362402 0.5591633 0.4822194
## 2 0.3394258 0.6165766 0.4749334 0.6781441 0.4366665 0.5591093 0.4805307
## 3 0.3386265 0.6118040 0.4722605 0.6772330 0.4360310 0.5610235 0.4806852
## 4 0.3381357 0.6096086 0.4693841 0.6687760 0.4213760 0.5598039 0.4784982
## 5 0.3379156 0.6102082 0.4699688 0.6689668 0.4356073 0.5615684 0.4809941
## 6 0.3406562 0.6059430 0.4642055 0.6655310 0.4338500 0.5523526 0.4505929
##       kp1_y     kp2_y     kp3_y     kp4_y     kp5_y     kp6_y     kp7_y
## 1 0.3828655 0.3538715 0.3582214 0.3297369 0.3400428 0.3940416 0.4116655
## 2 0.3862496 0.3568694 0.3613199 0.3316347 0.3397953 0.3932600 0.4108571
## 3 0.3842199 0.3555542 0.3595651 0.3297170 0.3386594 0.3919772 0.4097573
## 4 0.3876366 0.3572809 0.3631046 0.3304182 0.3423170 0.3910375 0.4099642
## 5 0.3816542 0.3537847 0.3582130 0.3290937 0.3395939 0.3904398 0.4096211
## 6 0.3875708 0.3607277 0.3649541 0.3304738 0.3427933 0.3885977 0.4089984
##       kp8_y     kp9_y    kp10_y    kp11_y    kp12_y    kp13_y    kp14_y
## 1 0.5216534 0.5355401 0.6526011 0.6306298 0.6974007 0.6894653 0.5340295
## 2 0.5208964 0.5338422 0.6529879 0.6289278 0.6980432 0.6903628 0.5327339
## 3 0.5206015 0.5324742 0.6543402 0.6309026 0.6951404 0.6880409 0.5345150
## 4 0.5178449 0.5305706 0.6518627 0.6308511 0.6992487 0.6923172 0.5312581
## 5 0.5155663 0.5296328 0.6510806 0.6313198 0.6969516 0.6895905 0.5312496
## 6 0.5160381 0.5272955 0.6512745 0.6292999 0.6969529 0.6873756 0.5296530
##      kp15_y    kp16_y    kp17_y  kp1_conf  kp2_conf  kp3_conf  kp4_conf
## 1 0.7561160 0.7344654 0.7249266 0.6846372 0.7523584 0.4823741 0.6902475
## 2 0.7528221 0.7332112 0.7237307 0.6780812 0.6624680 0.6396530 0.7131832
## 3 0.7530572 0.7352153 0.7234242 0.6876624 0.7296401 0.5587097 0.6996058
## 4 0.7548109 0.7370406 0.7445073 0.6828207 0.6788521 0.6495970 0.6651469
## 5 0.7525228 0.7367203 0.7423019 0.6747093 0.7821043 0.4424501 0.5757949
## 6 0.7563542 0.7295856 0.7229223 0.6674658 0.5749986 0.5770948 0.5006248
##    kp5_conf  kp6_conf  kp7_conf  kp8_conf  kp9_conf kp10_conf kp11_conf
## 1 0.7582073 0.9098083 0.8618663 0.7833734 0.7944545 0.7437379 0.7171616
## 2 0.7674576 0.9208025 0.8600967 0.7651477 0.7959530 0.7053676 0.7465761
## 3 0.7793038 0.9158441 0.8607285 0.8051816 0.7388552 0.7564675 0.7575192
## 4 0.5641937 0.8905997 0.8600141 0.7657987 0.7246420 0.7261182 0.7549317
## 5 0.6841950 0.8731019 0.8712515 0.7445847 0.7165044 0.7513632 0.7503713
## 6 0.5303608 0.8094112 0.8147650 0.7557443 0.7018427 0.7242407 0.7847462
##   kp12_conf kp13_conf  kp14_conf  kp15_conf  kp16_conf  kp17_conf
## 1 0.6718510 0.7742214 0.02469357 0.02170779 0.01254712 0.01522222
## 2 0.6364603 0.7613094 0.02760529 0.01941576 0.01317525 0.01731255
## 3 0.6677965 0.7748846 0.02868244 0.02086468 0.01496059 0.01781935
## 4 0.6498149 0.7826247 0.02684401 0.01781688 0.01437873 0.01346653
## 5 0.6493299 0.7730550 0.02770437 0.01973066 0.01701542 0.01587264
## 6 0.6595952 0.7522419 0.02596327 0.02330721 0.02279426 0.01497867</code></pre>
<p>The new data frame contains the following variables:</p>
<p>Metadata:</p>
<ul>
<li><code>file_name</code> = name of the .json file (our data source)</li>
<li><code>frame</code> = frame identifier, increasing number in the order of their appearance</li>
<li><code>timestamp</code> = timestamp of the frame within the video sequence</li>
</ul>
<p>Key point data:</p>
<ul>
<li><code>kp1_x</code> = x-coordinate of key point 1 (nose)</li>
<li><code>kp2_x</code> = x-coordinate of key point 2 (left eye)</li>
<li><code>kp3_x</code> = x-coordinate of key point 3 (right eye)</li>
<li>…</li>
<li><code>kp17_x</code> = x-coordinate of key point 17 (right ankle)</li>
<li><code>kp1_y</code> = y-coordinate of key point 1 (nose)</li>
<li><code>kp2_y</code> = y-coordinate of key point 2 (left eye)</li>
<li><code>kp3_y</code> = y-coordinate of key point 3 (right eye)</li>
<li>…</li>
<li><code>kp17_y</code> = y-coordinate of key point 17 (right ankle)</li>
<li><code>kp1_conf</code> = confidence for key point 1 (nose)</li>
<li><code>kp2_conf</code> = confidence for key point 2 (left eye)</li>
<li><code>kp3_conf</code> = confidence for key point 3 (right eye)</li>
<li>…</li>
<li><code>kp17_conf</code> = confidence for key point 17 (right ankle)</li>
</ul>
</div>
<div id="visualizing-pose-estimation-data" class="section level3">
<h3>Visualizing pose estimation data</h3>
<p>Next, we want to get a feel for the data by looking at it visually. We want to see what the raw pose estimation data looks like and compare it to the video frames. I extracted the frames of both videos and stored them in <code>videos/frames_speech1</code> and <code>videos/frames_speech2</code>, respectively.</p>
<p>We will look at the first frame of our second video (that is, <code>videos/speech2_klaus_ernst.mp4</code>). Let’s first inspect the frame itself. I stored all frames of the two video sequences in our repository (in <code>videos/frame_speech1</code> and <code>videos/frame_speech2</code>, respectively). To plot the image in R, we start by loading the image into our environment using the <code>load.image()</code>-function provided by the <code>imager</code>-package.</p>
<pre class="r"><code>frame_number &lt;- 1 # you can change this number to inspect other frames

frame_filename &lt;- paste0(&quot;frame&quot;,
                         str_pad(frame_number, 5, pad = &quot;0&quot;),
                         &quot;.png&quot;)

image &lt;- imager::load.image(paste0(&quot;./computationally-pose-estimation/videos/frames_speech2/&quot;,
                                   frame_filename))</code></pre>
<p>What is the size of this image?</p>
<pre class="r"><code>x_size &lt;- dim(image)[1]
y_size &lt;- dim(image)[2]

x_size</code></pre>
<pre><code>## [1] 720</code></pre>
<pre class="r"><code>y_size</code></pre>
<pre><code>## [1] 400</code></pre>
<p>Plotting the image is actually quite easy:</p>
<pre class="r"><code>plot(image,
     axes = T)</code></pre>
<p><img src="/article/computationally-pose-estimation_files/figure-html/plot-image-1.png" width="672" /></p>
<p>Notice the y-axis: The origin point of the frame is in the upper left corner, with values on both axes increasing downwards (y-axis) and rightwards (x-axis). This is a convention that we need to keep in mind. You will see that it also affects our key point representations.</p>
<p>Next, we will look at the key point data of that first frame. First, we extract the key points of the first frame from our time series of key points.</p>
<pre class="r"><code># We want to plot one frame, so let&#39;s get the data for one frame
keypoints &lt;- speech2_keypoints[speech2_keypoints$frame == frame_number,]

keypoints</code></pre>
<pre><code>##             file_name frame timestamp    kp1_x     kp2_x     kp3_x    kp4_x
## 1 speech2_klaus_ernst     1      0.04 0.448366 0.4686418 0.4641348 0.524352
##       kp5_x     kp6_x     kp7_x     kp8_x     kp9_x    kp10_x    kp11_x
## 1 0.5150961 0.6155975 0.4670474 0.6058111 0.4096422 0.4812012 0.3233894
##      kp12_x    kp13_x    kp14_x   kp15_x   kp16_x    kp17_x     kp1_y    kp2_y
## 1 0.5811864 0.4761012 0.4216525 0.349405 0.461869 0.3719398 0.3608708 0.344443
##       kp3_y     kp4_y     kp5_y     kp6_y     kp7_y     kp8_y     kp9_y
## 1 0.3425739 0.3604536 0.3542357 0.4422205 0.4271012 0.5979796 0.5254039
##      kp10_y   kp11_y    kp12_y    kp13_y    kp14_y    kp15_y    kp16_y
## 1 0.6254648 0.474841 0.6754902 0.6661831 0.6479467 0.6561794 0.6459582
##      kp17_y  kp1_conf  kp2_conf kp3_conf  kp4_conf  kp5_conf  kp6_conf
## 1 0.6567515 0.6566256 0.6068109  0.49603 0.6411492 0.5322986 0.8388116
##    kp7_conf  kp8_conf  kp9_conf kp10_conf kp11_conf kp12_conf kp13_conf
## 1 0.6293974 0.4995181 0.7030932  0.690944 0.7124801 0.6853148   0.64159
##   kp14_conf kp15_conf kp16_conf  kp17_conf
## 1 0.2697131 0.2958756 0.1492984 0.03688606</code></pre>
<p>The frame only shows upper body key points. Thus, it makes sense to subset the data to those upper body key points and discard all lower body key points.</p>
<p>Remember the list from above? <code>kp1</code> to <code>kp13</code> are upper body key points while <code>kp14</code> to <code>kp17</code> are lower body key points.</p>
<pre class="r"><code># we want to subset to the upper body
upper_body_pattern &lt;- paste0(&quot;kp&quot;, 1:13, &quot;_&quot;, collapse = &quot;|&quot;)
keypoints &lt;- keypoints[stringr::str_detect(names(keypoints), 
                                           upper_body_pattern)]</code></pre>
<p>To make plotting a bit easier, we will reorganize the remaining data. We want a matrix that stores the x-coordinates of all key points in one column, and the y-coordinates in another column:</p>
<pre class="r"><code>kp_x &lt;- t(keypoints[, stringr::str_detect(names(keypoints), &quot;_x&quot;)])
kp_y &lt;- t(keypoints[, stringr::str_detect(names(keypoints), &quot;_y&quot;)])

kp_frame &lt;- data.frame(cbind(kp_x, kp_y))
colnames(kp_frame) &lt;- c(&quot;x&quot;, &quot;y&quot;)

kp_frame</code></pre>
<pre><code>##                x         y
## kp1_x  0.4483660 0.3608708
## kp2_x  0.4686418 0.3444430
## kp3_x  0.4641348 0.3425739
## kp4_x  0.5243520 0.3604536
## kp5_x  0.5150961 0.3542357
## kp6_x  0.6155975 0.4422205
## kp7_x  0.4670474 0.4271012
## kp8_x  0.6058111 0.5979796
## kp9_x  0.4096422 0.5254039
## kp10_x 0.4812012 0.6254648
## kp11_x 0.3233894 0.4748410
## kp12_x 0.5811864 0.6754902
## kp13_x 0.4761012 0.6661831</code></pre>
<p>Now, we are finally in a position where we can visualize the upper key points of our frame:</p>
<pre class="r"><code>plot(x = kp_frame$x,
     y = kp_frame$y,
     xlim = c(0, 1),
     ylim = c(0, 1),
     asp = 1,
     pch = 19,
     las = 1,
     xlab = &quot;x-coordinates&quot;,
     ylab = &quot;y-coordinates&quot;)
grid()</code></pre>
<p><img src="/article/computationally-pose-estimation_files/figure-html/plot-keypoints-1-1.png" width="672" /></p>
<p>Okay, this does not look very helpful yet. Let’s add lines between some key points to create an image closer to the one we’ve seen on the slides earlier.</p>
<pre class="r"><code># this list stores all key points we would like to connect

edges &lt;- list(c(1, 2),    # nose - left eye
              c(1, 3),    # nose - right eye
              c(2, 4),    # left eye - left ear
              c(3, 5),    # right eye - right ear
              c(1, 6),    # nose - left shoulder
              c(1, 7),    # nose - right shoulder
              c(6, 8),    # left shoulder - left elbow
              c(8, 10),   # left elbow - left wrist
              c(7, 9),    # right shoulder - right elbow
              c(9, 11),   # right elbow - right wrist
              c(6, 7),    # left shoulder - right shoulder
              c(6, 12),   # left shoulder - left hip
              c(7, 13),   # right shoulder - right hip
              c(12, 13),  # left hip - right hip
              c(12, 14),  # left hip - left knee
              c(14, 16),  # left knee - left ankle
              c(13, 15),  # right hip - right knee
              c(15, 17))  # right knee - right ankle


# 1) We start with an empty plot:
plot(x = kp_frame$x,
     y = kp_frame$y,
     type = &quot;n&quot;,
     xlim = c(0, 1),
     ylim = c(0, 1),
     asp = 1,
     las = 1,
     xlab = &quot;x-coordinates&quot;,
     ylab = &quot;y-coordinates&quot;)
grid()

# 2) Next, we add all lines to the plot:
for (e in 1:length(edges)) {
  p1 &lt;- kp_frame[edges[[e]][1], c(&quot;x&quot;, &quot;y&quot;)]
  p2 &lt;- kp_frame[edges[[e]][2], c(&quot;x&quot;, &quot;y&quot;)]
  
  if (all(!is.na(p1)) &amp; all(!is.na(p2))) {
    segments(
      x0 = as.numeric(p1$x),
      y0 = as.numeric(p1$y),
      x1 = as.numeric(p2$x),
      y1 = as.numeric(p2$y),
      lwd = 2,
      col = viridis(1, 0.5)
    )
  }
}

# 3) And add the key points on top:
points(x = kp_frame$x,
       y = kp_frame$y,
       pch = 19,
       cex = 1.25,
       col = viridis(1))</code></pre>
<p><img src="/article/computationally-pose-estimation_files/figure-html/plot-keypoints-2-1.png" width="672" /></p>
<p>This figure looks better, but the speaker is upside down. Why? Because, as we’ve seen earlier, the y-axis is reversed. We can fix this quite easily by adjusting <code>ylim</code>:</p>
<pre class="r"><code>plot(x = kp_frame$x,
     y = kp_frame$y,
     type = &quot;n&quot;,
     xlim = c(0, 1),
     ylim = c(1, 0), # changing this from c(0,1) to c(1,0) fixes the y-axis
     asp = 1,
     las = 1,
     xlab = &quot;x-coordinates&quot;,
     ylab = &quot;y-coordinates&quot;)
grid()
  

# The rest of the code remains the same:

for (e in 1:length(edges)) {
  p1 &lt;- kp_frame[edges[[e]][1], c(&quot;x&quot;, &quot;y&quot;)]
  p2 &lt;- kp_frame[edges[[e]][2], c(&quot;x&quot;, &quot;y&quot;)]
  
  if (all(!is.na(p1)) &amp; all(!is.na(p2))) {
    segments(
      x0 = as.numeric(p1$x),
      y0 = as.numeric(p1$y),
      x1 = as.numeric(p2$x),
      y1 = as.numeric(p2$y),
      lwd = 2,
      col = viridis(1, 0.5)
    )
  }
}

points(x = kp_frame$x,
       y = kp_frame$y,
       pch = 19,
       cex = 1.25,
       col = viridis(1))</code></pre>
<p><img src="/article/computationally-pose-estimation_files/figure-html/plot-keypoints-3-1.png" width="672" /></p>
<p>This looks more like what we envisioned. We can also look at the frame and the key point representation side by side:</p>
<pre class="r"><code># create two columns
par(mfrow = c(1, 2))

# plot the frame:
plot(image,
     axes = T)

# plot the key point representation
plot(x = kp_frame$x,
     y = kp_frame$y,
     asp = 1,
     xlim = c(0, 1),
     ylim = c(1, 0), # changing this from c(0,1) to c(1,0) fixes the y-axis
     axes = T,
     xlab = &quot;&quot;,
     ylab = &quot;&quot;,
     bty = &quot;n&quot;,
     las = 1)
grid()


for (e in 1:length(edges)) {
  p1 &lt;- kp_frame[edges[[e]][1], c(&quot;x&quot;, &quot;y&quot;)]
  p2 &lt;- kp_frame[edges[[e]][2], c(&quot;x&quot;, &quot;y&quot;)]
  
  if (all(!is.na(p1)) &amp; all(!is.na(p2))) {
    segments(
      x0 = as.numeric(p1$x),
      y0 = as.numeric(p1$y),
      x1 = as.numeric(p2$x),
      y1 = as.numeric(p2$y),
      lwd = 2,
      col = viridis(1, 0.5)
    )
  }
}

points(x = kp_frame$x,
       y = kp_frame$y,
       pch = 19,
       cex = 1.25,
       col = viridis(1))</code></pre>
<p><img src="/article/computationally-pose-estimation_files/figure-html/plot-keypoints-4-1.png" width="672" /></p>
<p>Or, we can plot the key points on top of the frame. To do so, we need to rescale the key points. The key points are scaled between zero and one, whereas our images are scaled according to their pixel size, i.e., 720x400.</p>
<p>The following two functions rescale the x- and y-values of our key point representations so that they match the scale of the frame:</p>
<pre class="r"><code>scale_x &lt;- function(x, x_size){x*x_size}
scale_y &lt;- function(y, x_size, y_size){y*x_size-((x_size-y_size)/2)}

kp_frame$x_scaled &lt;- scale_x(x = kp_frame$x, x_size = x_size)
kp_frame$y_scaled &lt;- scale_y(y = kp_frame$y, x_size = x_size, y_size = y_size)</code></pre>
<p><img src="/article/computationally-pose-estimation_files/figure-html/plot-keypoints-5-1.png" width="672" /></p>
<p>I hope visualizing individual frames gave you a feeling for the data. We now calculate two body language indicators from a time series of key point representations. That is, we will calculate a measure of gesticulation and a measure of posture both speech sequences.</p>
</div>
<div id="calculating-measure-of-gesticulation-and-posture" class="section level3">
<h3>Calculating measure of gesticulation and posture</h3>
<div id="gesticulation" class="section level4">
<h4>Gesticulation</h4>
<p>Let’s recall how we would like to quantify the level of gesticulation shown by the speaker. I defined gesticulation as the dynamic use of gestures, specifically through hand movement. We will use the fact that the distances of hand locations between frames capture hand movement. The more a speaker moves their hands, the higher the distance of hand locations between two consecutive frames. We construct our <strong>measure of gesticulation</strong> as the <strong>average between-frame distance of the speaker’s left and right hand</strong>.</p>
<p>To calculate between-frame distances of key points, we need a function to calculate Euclidean distances:</p>
<pre class="r"><code>euclidean_distance &lt;- function(x1, y1, x2, y2){
  dist &lt;- sqrt((x2-x1)^2 + (y2-y1)^2)
  return(dist)
}</code></pre>
<p>With this function at hand, we can code a function that calculates the distances of the left and right wrists between all frames of a video sequence:</p>
<pre class="r"><code>calculate_wrist_movement &lt;- function(kp_data){
  
  # LEFT WRIST (kp10)
  kp_data$left_wrist_movement &lt;- 
      euclidean_distance(x1 = kp_data$kp10_x, 
                         y1 = kp_data$kp10_y,
                         x2 = lead(kp_data$kp10_x), 
                         y2 = lead(kp_data$kp10_y))
  
  # RIGHT WRIST (kp11)
  kp_data$right_wrist_movement &lt;- 
      euclidean_distance(x1 = kp_data$kp11_x, 
                         y1 = kp_data$kp11_y,
                         x2 = lead(kp_data$kp11_x), 
                         y2 = lead(kp_data$kp11_y))
  
  return(kp_data)
}</code></pre>
<p>Let’s apply the function to both speeches:</p>
<pre class="r"><code>speech1_keypoints &lt;- calculate_wrist_movement(speech1_keypoints)
speech2_keypoints &lt;- calculate_wrist_movement(speech2_keypoints)</code></pre>
<p>The function added variables for left and right wrist movement:</p>
<pre class="r"><code>head(speech1_keypoints[, c(&quot;left_wrist_movement&quot;, &quot;right_wrist_movement&quot;)])</code></pre>
<pre><code>##   left_wrist_movement right_wrist_movement
## 1         0.007190624         0.0026941678
## 2         0.006933159         0.0021304339
## 3         0.004113916         0.0004935446
## 4         0.003140460         0.0005178051
## 5         0.005515408         0.0034044976
## 6         0.001626312         0.0041811225</code></pre>
<pre class="r"><code>head(speech2_keypoints[, c(&quot;left_wrist_movement&quot;, &quot;right_wrist_movement&quot;)])</code></pre>
<pre><code>##   left_wrist_movement right_wrist_movement
## 1        0.0078175540          0.017903471
## 2        0.0005784549          0.022548839
## 3        0.0040205017          0.011768234
## 4        0.0004598332          0.001570991
## 5        0.0034531529          0.007260658
## 6        0.0034903135          0.026569880</code></pre>
<p>We can visualize the times series of the left and right wrist movement variables:</p>
<p><img src="/article/computationally-pose-estimation_files/figure-html/plot-wrist-movement-1.png" width="672" /></p>
<p>Our quantification of wrist movement confirms two observations: First, we see more overall hand (wrist) movement in Speech 2 than in Speech 1. Second, Klaus Ernst gesticulates more strongly with his right hand than his left hand, resulting in more movement in his right wrist than his left wrist.</p>
<p>To see whether those observations are correct, let’s have a look at both video sequences again:</p>
<div style="display: flex; justify-content: space-between;">
<p><img src="/article/computationally-pose-estimation/figures/speech1_gabriela_heinrich.gif" alt="Speech 1 by Gabriela Heinrich" style="width: 48%; margin-right: 1%;">
<img src="/article/computationally-pose-estimation/figures/gifs/speech2_klaus_ernst.gif" alt="Speech 2 by Klaus Ernst" style="width: 48%;"></p>
</div>
<p>Both our observations are reflected in the videos! There is more overall hand movement in the speech by Klaus Ernst, and he gesticulates more strongly with his right hand than his left hand. To get our measure of gesticulation, all left to do is to summarize the times series of wrist movement by calculating the average movement of the left and right wrist in both speeches:</p>
<pre class="r"><code># Gesticulation speech 1
gesticulations_speech1 &lt;- 
  mean(c(speech1_keypoints$left_wrist_movement,
         speech1_keypoints$right_wrist_movement),
       na.rm = T)

# Gesticulation speech 2
gesticulations_speech2 &lt;- 
  mean(c(speech2_keypoints$left_wrist_movement,
         speech2_keypoints$right_wrist_movement),
       na.rm = T)

# which value should be higher?
gesticulations_speech1</code></pre>
<pre><code>## [1] 0.00428086</code></pre>
<pre class="r"><code>gesticulations_speech2</code></pre>
<pre><code>## [1] 0.01500968</code></pre>
<p>Here is a visual comparison of the result:</p>
<p><img src="/article/computationally-pose-estimation_files/figure-html/inspect-gesticulation-1.png" width="672" /></p>
<p>In line with what we would hope and expect, the gesticulation indicator suggests that Klaus Ernst (Speech 2) showed higher levels of gesticulation than Gabriela Heinrich (Speech 1).</p>
</div>
<div id="posture" class="section level4">
<h4>Posture</h4>
<p>Next, we calculate the measure of posture. While the gesticulation measure quantifies hand movement irrespective of its form, posture quantifies the extent to which speakers adopt body size-increasing postures. We quantify this by calculating the average height of the wrist relative to the shoulder. Our measure of posture is the average height of the higher wrist across all frames in a speech sequence.</p>
<p>We start with a function that calculates the height of the left and right wrist and determines which of both is higher in any given frame of a sequence:</p>
<pre class="r"><code>calculate_wrist_height &lt;- function(kp_data){
  
  # left shoulder: kp6
  # left wrist: kp10
  # right shoulder: kp7
  # right wrist: kp11
  
  # left height:
  kp_data$left_wrist_height &lt;- kp_data$kp6_y - kp_data$kp10_y
  
  # right wrist height:
  kp_data$right_wrist_height &lt;- kp_data$kp7_y - kp_data$kp11_y
  
  # height of the higher wrist
  kp_data$max_wrist_height &lt;- 
    apply(kp_data[, c(&quot;left_wrist_height&quot;, &quot;right_wrist_height&quot;)], 1, max)
  
  return(kp_data)
}</code></pre>
<p>…and apply the function:</p>
<pre class="r"><code>speech1_keypoints &lt;- calculate_wrist_height(speech1_keypoints)
speech2_keypoints &lt;- calculate_wrist_height(speech2_keypoints)</code></pre>
<p>Before averaging over all frames, let’s take a look at the time series again:</p>
<p><img src="/article/computationally-pose-estimation_files/figure-html/inspect-wrist-height-1.png" width="672" /></p>
<p>We observe three things: First, while Gabriela Heinrich raises her right wrist to some extent in the middle of the sequence, she never raises her wrists above her shoulders. Second, Klaus Ernst frequently raises both wrists above shoulder height in the first half of the sequence. Third, in the second half of the sequence, Klaus Ernst only raises his right wrist.</p>
<p>We can again confirm these observations by inspecting the videos:</p>
<div style="display: flex; justify-content: space-between;">
<p><img src="/article/computationally-pose-estimation/figures/speech1_gabriela_heinrich.gif" alt="Speech 1 by Gabriela Heinrich" style="width: 48%; margin-right: 1%;">
<img src="/article/computationally-pose-estimation/figures/speech2_klaus_ernst.gif" alt="Speech 2 by Klaus Ernst" style="width: 48%;"></p>
</div>
<p>The summary measure of posture averages the height of the higher wrist across all frames of a sequence:</p>
<pre class="r"><code>posture_speech1 &lt;- mean(speech1_keypoints$max_wrist_height)
posture_speech2 &lt;- mean(speech2_keypoints$max_wrist_height)

# which should be higher?
posture_speech1</code></pre>
<pre><code>## [1] -0.2037267</code></pre>
<pre class="r"><code>posture_speech2</code></pre>
<pre><code>## [1] -0.05291393</code></pre>
<p>Again, we’ll visually compare the result of both speeches:</p>
<p><img src="/article/computationally-pose-estimation_files/figure-html/inspect-posture-1.png" width="672" /></p>
<p>Both values are negative, indicating that, on average, both speakers held their wrists below their shoulders. Again, the results confirm our intuition, showing that Klaus Ernst made more use of body-size-increasing postures than Gabriela Heinrich.</p>
</div>
</div>
<div id="wrapping-up-and-further-reading" class="section level3">
<h3>Wrapping up and Further Reading<a name="furtherreadings"></a></h3>
<p>In this workshop, we learned how to apply the pose estimation model <code>MoveNet</code> to videos of political speech, explored the resulting data, and calculated measures of gesticulation and posture based on it.</p>
<p>In principal, the proposed method works for speeches in various parliaments and is not bound to the German Bundestag, as long as there is a camera that captures the speaker from a fixed position, preferably from the front. For a more detailed discussion on the application of pose estimation models for the analysis of politicians’ body language during political speech, please refer to the following paper:</p>
<!-- Add a list of further readings -->
<ul>
<li>Rittmann, Oliver (2024). A Measurement Framework for Computationally Analyzing Politicians’ Body Language. <em>OSF Preprint</em>, available at <a href="https://doi.org/10.31219/osf.io/9wynp">doi.org/10.31219/osf.io/9wynp</a>.</li>
</ul>
</div>
]]>
      </description>
    </item>
    
    <item>
      <title>A Hands-On Introduction to Artificial Neural Networks</title>
      <link>https://socialsciencedatalab.mzes.uni-mannheim.de/article/ann/</link>
      <pubDate>Tue, 18 Jul 2023 01:00:00 +0100</pubDate>
      
      <guid>https://socialsciencedatalab.mzes.uni-mannheim.de/article/ann/</guid>
      <description><![CDATA[
        </p>
<p>Neural networks are powerful machine learning algorithms that form the basis of many important technologies, including generative AI and computer vision. However, they are not as straight-forward to implement as many other machine learning techniques, like random forest or logistic regression. If you are a researcher interested in applying neural networks, this <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/">Methods Bites Tutorial</a> by <a href="https://www.mzes.uni-mannheim.de/d7/en/profiles/john-james-collins">John ‘Jack’ Collins</a> demonstrates how to get started with Artificial Neural Networks (ANN) and helps you easily prototype a neural network for your own use-case.</p>
<p>This article is aimed at researchers who may be familiar with python, know some basic machine learning (i.e. logistic regression or random forest), but have yet to try using neural networks. We will:</p>
<ul>
<li>Demonstrate how to quickly produce a model, train, validate, and evaluate it. We also show how to efficiently find the best hyperparameters.</li>
<li>Offer reuseable code which makes it easy to prototype ANNs in your own project.</li>
<li>Introduce you to libraries Keras and Tensorflow, which together make one of the most popular approaches to AI coding.</li>
<li>Finish by demonstrating how a Keras model can be saved and loaded, enabling you to deploy the model as you please.</li>
</ul>
<p>By way of example, we will step through a simple classification problem with the well-known ‘iris’ dataset. We close with a brief demonstration of how the same code can easily be re-used to accomplish other use cases as well.</p>
<p>If you would like to access the original <strong>code</strong>, the project is public on GitHub <a href="https://github.com/JackCollins91/DMU_ANN_Tutorial.git">here</a>.</p>
<div id="overview" class="section level3">
<h3>Overview</h3>
<ol style="list-style-type: decimal">
<li><a href="#introduction"><strong>Introduction</strong></a>
<ol style="list-style-type: decimal">
<li><a href="#what-are-anns">What are ANNs?</a></li>
<li><a href="#what-are-anns-useful-for">What are ANNs useful for?</a></li>
<li><a href="#how-do-i-get-started">How do I get started?</a></li>
</ol></li>
<li><a href="#setup"><strong>Setup</strong></a></li>
<li><a href="#data"><strong>Data</strong></a>
<ol style="list-style-type: decimal">
<li><a href="#import">Import</a></li>
<li><a href="#data-inspection">Data inspection</a></li>
</ol></li>
<li><a href="#modelling"><strong>Modelling</strong></a>
<ol style="list-style-type: decimal">
<li><a href="#a-simple-ann">A simple ANN</a></li>
<li><a href="#a-hypermodel-class">A hypermodel class</a></li>
<li><a href="#demonstrating-hypermodel-class">Demonstrating hypermodel class</a></li>
</ol></li>
<li><a href="#save-and-load-keras-models"><strong>Save and load Keras models</strong></a></li>
<li><a href="#other-use-cases"><strong>Other use cases</strong></a>
<ol style="list-style-type: decimal">
<li><a href="#binary-classification">Binary classification</a></li>
<li><a href="#regression">Regression</a></li>
</ol></li>
<li><a href="#conclusion"><strong>Conclusion</strong></a></li>
<li><a href="#further-reading"><strong>Further reading</strong></a></li>
<li><a href="#about-the-author"><strong>About the author</strong></a></li>
</ol>
</div>
<div id="introduction" class="section level3">
<h3>Introduction</h3>
<div id="what-are-anns" class="section level5">
<h5>What are ANNs?</h5>
<p>Even if you have no familiarity with the theory, you can still follow this tutorial, just review the following key points.</p>
<ul>
<li>An Artificial Neural Network is a type of supervised machine learner. It is trained on data and can then be used to make predictions about new data. In this tutorial, we will train an ANN to classify the species of flowers based on measurements of their petals.</li>
<li>In this tutorial, we introduce the simplest kind of ANN, a ‘Feed Forward Neural Network’ (FNN), also known as a ‘Multilayer Perceptron’ (MLP). There are many more complex types of ANN which are better-suited to more complex tasks, like time-series prediction (with Recurrent Neural Networks) and Image Processing (with Convolutional Neural Networks). These are great techniques to learn, and this tutorial offers a solid basis from which to continue learning about those elsewhere. In fact, you may even reuse the same code with minor adjustments to perform those exact techniques.</li>
<li>This tutorial is about stepping the reader through an example, and not about explaining what ANNs are. However, if you want to understand the theory, there are many resources available: We suggest <a href="https://www.youtube.com/watch?v=CqOfi41LfDw&amp;ab_channel=StatQuestwithJoshStarmer">this video series</a> for an introduction to the theory of ANNs. If you want a very detailed understanding and are willing to purchase it, we can also recommend this excellent <a href="https://www.udemy.com/course/deep-learning-tensorflow-2/">Udemy course</a>.</li>
<li>However, here’s a brief description of an FNN: An FNN is composed of computational units called ‘neurons’ which are grouped in sets called ‘layers.’ The inputs from the independent features are passed through the layers of neurons. The neurons fit weights to the inputs in a way that derives connections between the independent features and the dependant variable.</li>
<li>While ANNs are very well-suited for certain machine learning problems, in many scenarios simpler models, like logistic regression, may actually be better than ANNs. We’ll explain this in the next section.</li>
</ul>
</div>
<div id="what-are-anns-useful-for" class="section level5">
<h5>What are ANNs useful for?</h5>
<p>ANNs excel at non-linearly separable problems. In the following figure, we can see an example of a non-linearly separable problem compared to one that is linear.</p>
<p><img src="../../../../../article/ann/fig1.png" width="100%" style="display: block; margin: auto;" /></p>
<p>With a linear problem, you can see how it would be possible to draw a line (straight or curvy) between the blue and red dots. This means we could derive a linear function (or a polynomial function) which takes the X and Y coordinates and outputs a classification for whether the dot is blue or red. But in the non-linear problem, there is no way a line (no matter how curvy) can separate the blue and red dots.</p>
<p>The functions which underpin ANNs allows for interactions between variables. This means the underlying function can fit to the X and Y inputs such that a dot is classified blue if the X coordinate is of a certain value AND the Y coordinates are within a certain range also. This innovation means that ANNs can fit to types of problems that are impossible for linear models.</p>
<p>However, ANNs are not always best. Because ANNs can fit so finely to data, it is vulnerable to overfitting. When a problem is linearly separable, it may often be better to use the simpler, and less overfit-prone model instead.</p>
<p>Those readers familiar with tree-based models might recognize that tree algorithms can also handle interactions between features. It is true that tree-based models are also good for non-linearly separable problems as well as ANNs. Tree-based models partition a feature space, such as the X and Y coordinate space in the diagram above, into subsections and classify points based on which subsection they fall into. A key difference betweens ANNs and trees, is that while tree-based models usually have distinct boundaries between subsections (a result of the binary decision functions in the trees), ANNs are better suited to yield varying probabilities for classification classes across the space. This is not to say tree-based models do not output probabilistic predictions (they do), just that ANNs can be better suited to finely tune those probabilities. See <a href="https://towardsdatascience.com/when-and-why-tree-based-models-often-outperform-neural-networks-ceba9ecd0fd8#:~:text=The%20primary%20difference%20in%20usage,power%20of%20tree%2Dbased%20methods.">this article</a> for a deeper discussion of the differences between tree-based models and neural networks.</p>
</div>
<div id="how-do-i-get-started" class="section level5">
<h5>How do I get started?</h5>
<p>In this tutorial, we will step through an example from which you can reuse this code in your own project. We will demonstrate how to use Keras and Tensorflow, together, this is the most common starting point for learning neural networks. Before we begin, here’s a quick introduction to these two packages.</p>
<div id="what-is-tensorflow" class="section level6">
<h6>What is Tensorflow?</h6>
<p><a href="https://www.tensorflow.org/about#:~:text=TensorFlow%20gives%20you%20the%20flexibility,fast%20debugging%2C%20use%20eager%20execution.">Tensorflow</a> is one of <a href="https://www.forbes.com/sites/janakirammsv/2020/11/27/tensorflow-turns-5five-reasons-why-it-is-the-most-popular-ml-framework/">the most popular</a> tools for developing neural networks. Tensorflow is not a library for making neural networks per se. Rather, Tensorflow is for efficiently handling the mathematic operations that neural networks rely on. ANNs utilize a lot of linear algebra, matrix operations and node-edge graph manipulations. Tensorflow provides a set of functionality to accomplish these operations efficiently with C++ code and allows the developer to use these functions with a python interface.</p>
</div>
<div id="what-is-keras" class="section level6">
<h6>What is Keras?</h6>
<p>As discussed, Tensorflow itself does not directly provide neural network functionality. Instead, the programmer who wants to develop an ANN will need another library. <a href="https://keras.io/why_keras/#:~:text=Keras%20prioritizes%20developer%20experience&amp;text=Keras%20follows%20best%20practices%20for,learn%20and%20easy%20to%20use.">Keras</a> provides programmers a way to declare what kind of ANN they want and then Keras accomplishes the interfacing with Tensorflow to generate the network. Tensorflow focusses on accomplishing the mathematics efficiently, while Keras provides programmers an easy way to develop their models.
Although Keras and Tensorflow are very often used together, they are actually independent. Keras can be used to interface with an alternative to Tensorflow, like Theano. Similarly, developers can use an alternatives to Keras, like TFLearn and still use Tensorflow in the backend.</p>
</div>
</div>
</div>
<div id="setup" class="section level3">
<h3>Setup</h3>
<p>Before writing the code, we import the necessary libraries and set a few configurations.</p>
<pre class="python"><code>### Libraries to pip install
# keras-tuner
# matplotlib
# numpy
# pandas
# scikit-learn
# tensorflow

import copy # helps make deep copies of keras models. 
import keras_tuner as kt # for hypertuning with keras models. 
import matplotlib # visualization library
import numpy as np # popular library for mathematic operations
import pandas as pd # popular library for data table manipulation
import shap # explained in &#39;interpretation&#39; section below
import shutil # file and directory manipulation
import sklearn as sk # for evaluation metrics and the example datasets. 
import tensorflow as tf 
import warnings

from keras.utils import np_utils
from matplotlib import pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import (
    confusion_matrix,
    ConfusionMatrixDisplay, 
    accuracy_score, 
    mean_absolute_error
    )
from tensorflow import keras
from tensorflow.keras.callbacks import EarlyStopping
from keras.losses import Loss
from keras.losses import SparseCategoricalCrossentropy


### Tensorflow issues some warnings but they are not 
# important for us, so we&#39;ll just suppress them. 
warnings.simplefilter(&quot;ignore&quot;)
tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)

### Setting random seed for reproducible results
tf.keras.utils.set_random_seed(42)</code></pre>
</div>
<div id="data" class="section level3">
<h3>Data</h3>
<p>In this section, we introduce the dataset for our experiment. We use the <a href="https://archive.ics.uci.edu/ml/datasets/iris">iris dataset</a>, in which we aim to classify which of three species of flower the subject is, based on measurements of petals and sepals.</p>
<div id="import" class="section level5">
<h5>Import</h5>
<pre class="python"><code>dataset = datasets.load_iris(return_X_y=False, as_frame=False)
X = dataset.data
y = dataset.target
### a keras utility function to one hot encode (OHE) the vector of 
# categories y.
y = np_utils.to_categorical(y) 

### The number of unique classes we seek to classify. 
# There are three species of iris in our dataset. 
n_classes = len(dataset.target_names) 

### ANNs require us to specify an &#39;input shape.&#39; If our input were 
# images, we might have a 2d input, but for this problem, 
# it is just the number of predictive features, which is 4.
input_shape = len(dataset.feature_names)

### This is a list of the titles of the features.
feature_names = dataset.feature_names 

### Test-train splitting
X_train, X_test, y_train, y_test = train_test_split(
    X, 
    y, 
    test_size=0.33, 
    random_state=42
    )</code></pre>
</div>
<div id="data-inspection" class="section level5">
<h5>Data inspection</h5>
<div id="the-predictive-features-x" class="section level6">
<h6>The predictive features (X)</h6>
<p>Our X value is a matrix of floats where each row is a flower and each column is a measurement.</p>
<pre class="python"><code>pd.DataFrame(X,columns = feature_names).head(5)</code></pre>
<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th>
</th>
<th>
sepal length (cm)
</th>
<th>
sepal width (cm)
</th>
<th>
petal length (cm)
</th>
<th>
petal width (cm)
</th>
</tr>
</thead>
<tbody>
<tr>
<th>
0
</th>
<td>
5.1
</td>
<td>
3.5
</td>
<td>
1.4
</td>
<td>
0.2
</td>
</tr>
<tr>
<th>
1
</th>
<td>
4.9
</td>
<td>
3.0
</td>
<td>
1.4
</td>
<td>
0.2
</td>
</tr>
<tr>
<th>
2
</th>
<td>
4.7
</td>
<td>
3.2
</td>
<td>
1.3
</td>
<td>
0.2
</td>
</tr>
<tr>
<th>
3
</th>
<td>
4.6
</td>
<td>
3.1
</td>
<td>
1.5
</td>
<td>
0.2
</td>
</tr>
<tr>
<th>
4
</th>
<td>
5.0
</td>
<td>
3.6
</td>
<td>
1.4
</td>
<td>
0.2
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="the-dependant-y" class="section level6">
<h6>The dependant (y)</h6>
<p>For each row in X, there is a corresponding item in the vector y, which represents the species of flower. There are three unique species in our dataset as shown in the table below.
However, our dependant y must come in the form of a One Hot Encoded (OHE) matrix, not a vector. In this format, there are three columns with a zero or one value to indicate which species of flower is this subject.</p>
<pre class="python"><code>pd.DataFrame(y[:5],columns = dataset.target_names)</code></pre>
<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
<thead>
<tr style="text-align: right;">
<th>
</th>
<th>
setosa
</th>
<th>
versicolor
</th>
<th>
virginica
</th>
</tr>
</thead>
<tbody>
<tr>
<th>
0
</th>
<td>
1.0
</td>
<td>
0.0
</td>
<td>
0.0
</td>
</tr>
<tr>
<th>
1
</th>
<td>
1.0
</td>
<td>
0.0
</td>
<td>
0.0
</td>
</tr>
<tr>
<th>
2
</th>
<td>
1.0
</td>
<td>
0.0
</td>
<td>
0.0
</td>
</tr>
<tr>
<th>
3
</th>
<td>
1.0
</td>
<td>
0.0
</td>
<td>
0.0
</td>
</tr>
<tr>
<th>
4
</th>
<td>
1.0
</td>
<td>
0.0
</td>
<td>
0.0
</td>
</tr>
</tbody>
</table>
</div>
<p>Let us inspect this same data when converted to an array, this will be used to ‘flatten’ the matrix form of y to a vector. Each row is a ‘setosa’ (0).</p>
<pre class="python"><code>y[:5].argmax(axis=1)</code></pre>
<pre><code>array([0, 0, 0, 0, 0], dtype=int64)</code></pre>
</div>
</div>
</div>
<div id="modelling" class="section level3">
<h3>Modelling</h3>
<div id="a-simple-ann" class="section level5">
<h5>A simple ANN</h5>
<p>If we want to develop a simple ANN, the Keras library makes this extremely simple. The code stub below defines a function <code>get_simple_ann()</code> and the following is the stp by step of that function:</p>
<ol style="list-style-type: decimal">
<li>We instantiate an empty ‘sequential’ model, so-called because each layer of neurons (which we shall add shortly) will feed into the subsequent layer (hence why this is also called a ‘Feed Forward’ network).</li>
<li>We must populate our empty Sequential model with layers. Here we will use ‘Dense’ layers, which are simply arrays of neurons. This is the most basic type of neural network layer.</li>
<li>After the Dense layer, we add a ‘Dropout’ layer. The Dropout layer is like a gatekeeper: it selects certain neurons in the previous layer and blocks its input from going to the next layer. This nullification is helpful for avoiding overfitting caused by neurons over-weighting certain signals in the data. Dropout layers use a fitting process to determine which neurons are the best to ‘drop’ to improve predictive performance.</li>
<li>We repeat steps 2 &amp; 3 to add another Dense-Dropout combo to the network. ANNs typically feature more than one layer of neurons to enable more interactions between features.</li>
<li>We add the output layer. The shape of the output layer is important because it determines the format of the predictions. Because we have three classes in our classification problem, we want three outputs: each one will yield a float which represents the probability that a given case is of the corresponding species.
If this were a regression problem, or a binary regression problem, we might only want one output unit, which would output just the regression value, or the probability of the binary case being a positive.</li>
<li>Once we have used the <code>Sequential</code> object to define the architecture of our model (meaning the number layers and the composition of neurons, etc), we need to ‘compile’ the object, which means to let Tensorflow convert the python code of our model into the lower-level ‘Tensors’ which can be more efficiently computed once we are ready to train this model.</li>
</ol>
<div id="terminology" class="section level6">
<h6>Terminology</h6>
<ul>
<li><p>Batch size: This parameter instructs the ANN to trial that number of cases with a set of
random weights before trialing another batch with a new set of weights. A large batch size enhances computational efficiency, at the potential cost of accuracy.</p></li>
<li><p>Epochs: The number of times the ANN shall repeatedly use the training data to find the optimum set of weights.
A large number of epochs may increase accuracy, but takes longer to compute.</p></li>
<li><p>Optimizer: In the fitting process, random weights are trialed for every neuron and the best combination is selected. However, trying every possible random combination is very inefficient. Instead, optimizers apply algorithms that aim to discover which combinations of parameters for each node is best. Instead of randomly selecting weights, the optimizer applies a guided process to discovering the best settings. These algorithms for determining the optimum weights are called ‘gradient descent’ algorithms.</p></li>
<li><p>Learning rate: The incremental value by which the optimizer varies weights. A larger learning rate may be quicker to compute, but less accurate.</p></li>
<li><p>Loss Function: During the fitting process, the loss function is how we calculate if a given trial was better or worse that others. Here we use ‘Binary Cross Entropy’ which is a common selection for classification problems. Mean-squared error would be an appropriate choice for regression problems.</p></li>
<li><p>Metrics: Machine learning practitioners know there are many ways to evaluate a model’s performance. We can record multiple metrics to evaluate performance. For example, we can examine the model’s accuracy (the portion of correct predictions) and the recall (the portion of correctly classified cases of a given class among all cases of that class).</p></li>
</ul>
<pre class="python"><code>def get_simple_ann(
        input_shape: int|tuple,
        output_shape:int|tuple
        ) -&gt; keras.models.Sequential:
    &quot;&quot;&quot;
    This function returns an instance of a 
    Keras Sequential model which is ready to 
    fit with training data.
    
    Args:
    input_shape: int or tuple. Describes the 
    dimensions of a single case of input.
    output_shape: int or tuple. The desired 
    shape of output. 
    
    Returns:
    A compiled instance of a keras Sequential model
    &quot;&quot;&quot;

    ### Step 1: Instantiate an empty Sequential model object
    ann = keras.models.Sequential()
    
    ### Step 2: Adding the input layer with same number 
    # of nodes as we have input features
    ann.add(
        keras.layers.Dense(
            units = 16,
            activation=&#39;relu&#39;, 
            input_dim=input_shape
            )
        )
    
    # Step 3: Adding 10% dropout to prevent overfitting
    ann.add(keras.layers.Dropout(rate=0.1))
    
    ### Step 4: Adding another Dense-Dropout combo to 
    # make a second layer in the network
    ann.add(
            keras.layers.Dense(
            units = 16, 
            activation=&#39;relu&#39;
            )
        )
    ann.add(keras.layers.Dropout(rate=0.1))
    
    ### Step 5: Adding the output layer. This must match the 
    # format of our y variable
    ann.add(
            keras.layers.Dense(
            units = output_shape, 
            activation=&#39;softmax&#39;
            )
        )
    
    ### Step 6: Once we have defined the architecture of 
    # our ANN, we call compile() to convert the model to tensors.
    ann.compile(
        optimizer=&#39;adam&#39;, 
        loss=&#39;binary_crossentropy&#39;, 
        metrics=[&#39;accuracy&#39;]
        )
    return ann

### Calling our function
simple_ann = get_simple_ann(
    input_shape = input_shape,
    output_shape = n_classes
    )

### Fitting to training data
simple_ann.fit(
    X_train,
    y_train,
    batch_size=100, 
    epochs=150,
    verbose=False
    )

### Making predictions
# argmax() here is used to &#39;flatten&#39; the matrix form of the 
# dependant variable (as seen above) into a vector of integers 
# which correspond to classes. 
predictions = simple_ann.predict(X_test).argmax(axis=1) 

### Accuracy score
accuracy = accuracy_score(y_test.argmax(axis=1),predictions)
print(&quot;Accuracy of this model is: &quot;+&quot;{:1.2f}&quot;.format(accuracy*100)+&quot;%&quot;)</code></pre>
<pre><code>2/2 [==============================] - 0s 0s/step
Accuracy of this model is: 70.00%</code></pre>
</div>
<div id="visualizing-the-ann" class="section level6">
<h6>Visualizing the ANN</h6>
<p><img src="../../../../../socialsciencedatalab/article/ann/fig2.png" width="100%" style="display: block; margin: auto;" /></p>
<p>From the code above, we are constructing an ANN like the one in this diagram. We have four features in our data, and so we have four input nodes to receive each value. If you know the theory of ANNs, you know that each node will apply a weight and an activation function to the input to determine the number it will output to every node in the next layer. However, we use a dropout layer, which will select some nodes to nullify. We have two such layers. Finally, we have three output nodes, representing the probability that the given case is of a certain species. Therefore, for each case we predict, we output three numbers. We conclude that whichever of those three numbers is largest corresponds to the most likely species for the given case.</p>
</div>
<div id="evaluating-the-model" class="section level6">
<h6>Evaluating the model</h6>
<p>As we can see from the output above, this model was 70% accurate, meaning it classified 70% of cases as the correct species.</p>
</div>
<div id="why-we-need-hypertuning" class="section level6">
<h6>Why we need hypertuning</h6>
<p>Is 70% accuracy the best we can do? What if we had more than 12 nodes in each layer? Or fewer? What if we added more layers? Or increased the dropout rate?
Hypertuning is how we experimentally verify which parameters are best.</p>
<p>To accomplish this, we could try repeatedly fitting the model with every possible combination of parameters (this is called ‘Grid Search’). However, for many parameters, this would require a lot of fittings. Instead, the <a href="https://keras.io/guides/keras_tuner/getting_started/">‘keras tuner’</a> library was created to efficiently trial different hyperparameter selections.</p>
</div>
</div>
<div id="a-hypermodel-class" class="section level5">
<h5>A hypermodel class</h5>
<p>In this section, we provide the code for a hypermodel class. You can copy this class into your own project and try it. If you are familiar with inheritance, you could also extend this class for more complex functionality, such as time-series or computer vision problems. This class provides a solid base from which to trial many different kinds of ANNs and apply it to many kinds of machine learning tasks. You could easily reuse this class for regression, or a different classification problem.
The purpose of this class is to:
- Receive a dictionary describing the desired hyperparameters to explore
- Receive the parameters of how you wish to fit the model (epochs, batch size, etc)
- Efficiently discover the optimum hyperparameters
- Returns the ‘best’ discovered model</p>
<p>The following is a lot of code, so if you are not interested in the details, you can skip reading the following code block. In the next section, we shall demonstrate the use of the class, which you can go on to reading instead. Like many classes, you do not necessarily need to know all the details of <em>how</em> it works, only how to use it.</p>
<p>If you would like to understand this class in detail, we recommend reading the documentation on the <a href="https://keras.io/guides/keras_tuner/getting_started/">keras-tuner library</a>.</p>
<p>To help explain the class, we also introduce some more terminology.</p>
<div id="terminology-1" class="section level6">
<h6>Terminology</h6>
<ul>
<li>Objective: One of the metrics. The ‘best’ model is the one with the highest value.</li>
<li>Hypergrid: Is a dictionary-like structure that specifies what values for each parameter to trial.</li>
<li>Callbacks: In this context, the callback is a special type of function called at the end of each epoch. In this example, we use ‘EarlyStopping,’ which is helpful for reducing computation time. With early stopping, once each additional epoch only improves the objective slightly, we conclude we have reached a point of diminishing return and finish the fitting process without completing every epoch.</li>
<li>Width: The number of units is a particular layer.</li>
<li>Depth: The number of layers in an ANN.</li>
<li>Validation split: By default, the tuner selects the model with the best value for the objective as calculated on training data. However, we can optionally specify a validation split size, whereby a random sample of training data will be withheld, and we select the best model as validated against that holdout data. This can be helpful in avoiding overfitting to training data.</li>
<li>Activation function: a function which each node in a layer uses to convert the inputted values into the outputted values. A sigmoid function would be appropriate for the activation function of an output node doing classification, while no function would be appropriate for a regression. For a discussion on the different types of common functions, see <a href="https://towardsdatascience.com/activation-functions-neural-networks-1cbd9f8d91d6">this article</a>.</li>
</ul>
<pre class="python"><code>class ANNHyperModel(kt.HyperModel):
    &quot;&quot;&quot;
    Given a set of hyperparameters to explore, 
    uses keras-tuner to discover the best settings 
    and returns the optimum model.
    &quot;&quot;&quot;
    
    _default_hypergrid = {
            &#39;width&#39;:{
                &#39;min&#39;:12,
                &#39;max&#39;:12,
                &#39;step&#39;:1
                },
            &#39;depth&#39;:{
                &#39;min&#39;:1,
                &#39;max&#39;:1,
                &#39;step&#39;:1
                },
            &#39;droprate&#39;:{
                &#39;min&#39;:.8,
                &#39;max&#39;:.8,
                &#39;step&#39;:.1
                },
            &#39;activation&#39;:{
                &#39;values&#39;:[&#39;relu&#39;]
            },
            &#39;learning_rate&#39;:{
                &#39;values&#39;:[1e-2]
            }
        }
    &quot;&quot;&quot;If the user does not input values for the 
    following hyperparameters, we use these values by default.&quot;&quot;&quot;
    
    def __init__(
                self,
                input_shape:tuple|int,
                n_outputs:int,
                hypergrid:dict = None,
                output_activation:str = &#39;sigmoid&#39;,
                epochs:int = 1,
                batch_size:int = 100,
                optimizer_class = keras.optimizers.Adam,
                loss: Loss = SparseCategoricalCrossentropy(
                                        from_logits=True
                                        ),
                metrics: list[str] = [&#39;accuracy&#39;],
                objective:str = &quot;accuracy&quot;,
                directory:str = None,
                project_name:str = &quot;kt_hyperband&quot;,
                factor:int = 3,
                verbose:int = 0,
                callbacks:list = []
    ):
        &quot;&quot;&quot;
        Args:
        - input_shape (tuple | int): Dimensions of a single input case. 
        Provide an int of n if one case is a 1-d array of length n.

        - n_outputs (int): Number of output nodes. One for regression or 
        binary-classification. Multiple for 
        multi-regression/multi-classification.

        - hypergrid (dict, optional): Dictionary describing hyperparams 
        to trial, see default &#39;_default_hypergrid&#39; for example. 
        Defaults to None.

        - output_activation (str, optional): Output layer activation 
        function. Defaults to &#39;sigmoid&#39;.

        - epochs (int, optional): Training epochs. Defaults to 1.

        - batch_size (int, optional): training batch size. 
        Defaults to 100.

        - optimizer_class (_type_, optional): Optimizer class. 
        Input the class type, not an instantiation. Defaults to 
        keras.optimizers.Adam.

        - loss (tf.keras.losses.Loss, optional): training loss function. 
        Defaults to 
        keras.losses.SparseCategoricalCrossentropy(from_logits=True).
        
        - metrics (list[str], optional): Training metrics. Defaults 
        to [&#39;accuracy&#39;].
        
        - objective (str, optional): Must be included in &#39;metrics.&#39; 
        Tuning process will select the model with the best performance
        by this metric. Defaults to &quot;accuracy&quot;.
        
        - directory (str, optional): When used, tuning process creates 
        a directory to hold data on disk. Deleted afterwards. 
        Be careful not to use existing dir name. Defaults to &quot;None&quot;.
        
        - project_name (str, optional): File naming stem in directory. 
        Defaults to &quot;kt_hyperband&quot;.

        -factor (int, optional): Reduction factor for epochs at each 
        tuning trial. Defaults to 3.

        -verbose (int, optional): Set to true to print details of 
        fitting process. Defaults to 0.
        
        - callbacks (list, optional): A keras.callback function called 
        after each epoch. Defaults to [].
        &quot;&quot;&quot;
        self.input_shape = input_shape
        self.n_outputs = n_outputs
        self.hp = kt.HyperParameters()
        self.hypergrid = hypergrid
        self.output_activation = output_activation
        self.epochs = epochs
        self.optimizer_class = optimizer_class
        self.loss = loss
        self.metrics = metrics
        self.objective = objective
        self.callbacks = callbacks
        self.directory = directory
        self.project_name = project_name
        self.factor = factor
        self.verbose = verbose
        self.batch_size = batch_size
        self.history = None # null before training

    def get_hypergrid(self):
        &quot;&quot;&quot;Returns the user-specified hypergrid, plus the default items 
        (from _default_hypergrid) where the user did not specify 
        values&quot;&quot;&quot;
        output = copy.deepcopy(ANNHyperModel._default_hypergrid)
        if self.hypergrid is None:
             self.hypergrid = {}
        output.update(self.hypergrid)
        return output

    ### If you want to try more complex types of ANN, you should extend 
    # this class and overwrite this function with your own logic. 
    # For example, instead of adding Dense layers, you could add LSTM 
    # layers instead to create an RNN. 
    def build(self,hp:kt.HyperParameters):
        &quot;&quot;&quot;Using an instance of kt.HyperParameters, we build and 
        compile a Keras Sequential instance with 
        some parameters filled by HyperParameter objects instead of 
        literal values. The Keras Hyperband object can then execute 
        a search function using this hypermodel to discover the 
        best model setting.&quot;&quot;&quot;

        ### here we take the hypergrid and use it to construct a set of 
        # kt.HyperParameter objects
        hypergrid = self.get_hypergrid()
        width_hyperparam = hp.Int(
              &#39;units&#39;, 
              min_value=hypergrid[&#39;width&#39;][&#39;min&#39;], 
              max_value=hypergrid[&#39;width&#39;][&#39;max&#39;], 
              step=hypergrid[&#39;width&#39;][&#39;step&#39;]
              )
        depth_hyperparam = hp.Int(
              &#39;layers&#39;, 
              min_value=hypergrid[&#39;depth&#39;][&#39;min&#39;], 
              max_value=hypergrid[&#39;depth&#39;][&#39;max&#39;], 
              step=hypergrid[&#39;depth&#39;][&#39;step&#39;]
              )
        droprate_hyperparam = hp.Float(
              &#39;droprate&#39;, 
              min_value=hypergrid[&#39;droprate&#39;][&#39;min&#39;], 
              max_value=hypergrid[&#39;droprate&#39;][&#39;max&#39;], 
              step=hypergrid[&#39;droprate&#39;][&#39;step&#39;]
              )
        activation_hyperparam = hp.Choice(
              &#39;activation&#39;, 
              values=hypergrid[&#39;activation&#39;][&#39;values&#39;]
              )
        learning_rate_hyperparam = hp.Choice(
              &#39;learning_rate&#39;, 
              values=hypergrid[&#39;learning_rate&#39;][&#39;values&#39;]
              )

        model = keras.Sequential()
        model.add(
              keras.layers.Input(
              shape=self.input_shape
              )
            )
        
        ### Add a Dense-Dropout combo for each layer in the depth param
        for layer_number in range(0,depth_hyperparam):
              model.add(
                    keras.layers.Dense(
                        units=width_hyperparam,
                        activation=activation_hyperparam,
                        name = &#39;Dense_&#39;+str(layer_number)
                        )
                    )
              model.add(
                   keras.layers.Dropout(droprate_hyperparam)
              )
        
        model.add(
                keras.layers.Dense(
                self.n_outputs,activation=self.output_activation
                )
            ) 

        model.compile(
            optimizer=self.optimizer_class(
                learning_rate=learning_rate_hyperparam
                ),
            loss=self.loss,
            metrics=self.metrics
            )
        ### This function must return an instance of a keras 
        # model that includes kt.Hyperparamater objects
        return model 
    
    def _hypertune(self,X_train, y_train, validation_size = 0):
        &quot;&quot;&quot;This function executes the hypertuning of the
          built hypermodel&quot;&quot;&quot;
        self.build(self.hp)
        
        self.tuner = kt.Hyperband(
            ### We can input an instance of the custom class here (self) 
            # and the &#39;build&#39; function will be called by keras 
            # Hyperband. 
                self, 
                objective= self.objective,
                max_epochs= self.epochs,
                factor=self.factor,
                hyperband_iterations=10,
                directory=self.directory,
                project_name=self.project_name
                )
        ### use validation split if validation_size &gt; 0
        validation_sets = None
        if validation_size &gt; 0:
            X_train, X_val, y_train, y_val = train_test_split(
                    X_train, 
                    y_train, 
                    test_size=validation_size
                    )
            validation_sets = (X_val,y_val)

        self.tuner.search(
               X_train,
               y_train, 
               epochs=self.epochs, 
               callbacks=self.callbacks, 
               verbose=self.verbose,
               batch_size=100,
               use_multiprocessing=True,
               validation_data=validation_sets
               )
        self.best_hyperparams=self.tuner.get_best_hyperparameters()[0]

        best_model = self.tuner.hypermodel.build(self.best_hyperparams)

        return best_model
    
    def execute(self,X,y,validation_size = 0):
        &quot;&quot;&quot;
        Fit the model on training data.

        Args:
            - X (_type_): predictors, ndarray
            - y (_type_): dependant, ndarray or 1-d array
            - validation_size (int, optional): If &gt; 0, tuner will 
            record validation performance of the objective. Set 
            objective to &#39;val_&lt;metric name&gt;&#39; to select best model 
            on validation data. Defaults to 0.

        Raises:
            - e: If errors occur during fitting, a &#39;final&#39; clause is 
            invoked to ensure the tuning directory is still deleted. 
        &quot;&quot;&quot;
        ### Use finally clause to ensure the tuning dir is deleted even 
        # if there is an error.This failsafe is used because &#39;leftover&#39; 
        # tuning directories from previous fittings can cause errors.
        try:
            self.model = self._hypertune(X,y,validation_size)
            self.history = self.model.fit(
                X,
                y, 
                batch_size=self.batch_size, 
                epochs=self.epochs,
                callbacks=self.callbacks, 
                verbose = self.verbose)
        except Exception as e:
            raise e
        
        finally:
            try:
                  shutil.rmtree(self.directory)
            except FileNotFoundError as e:
                 pass
</code></pre>
</div>
</div>
<div id="demonstrating-hypermodel-class" class="section level5">
<h5>Demonstrating hypermodel class</h5>
<p>You may skip reading the detail of how the <code>ANNHyperModel</code> class works and instead just review the following section to see a demonstration use case.</p>
<pre class="python"><code>hypergrid = {
                &#39;width&#39;:{
                    &#39;min&#39;:16,
                    &#39;max&#39;:64,
                    &#39;step&#39;:12
                    },
                &#39;depth&#39;:{
                    &#39;min&#39;:1,
                    &#39;max&#39;:4,
                    &#39;step&#39;:1
                    },
                &#39;droprate&#39;:{
                    &#39;min&#39;:0.1,
                    &#39;max&#39;:0.7,
                    &#39;step&#39;:0.2
                    },
                &#39;activation&#39;:{
                    &#39;values&#39;:[&#39;relu&#39;]
                }
            }

model = ANNHyperModel(
    input_shape =input_shape,
    n_outputs = n_classes,
    epochs = 150,
    loss = keras.losses.BinaryCrossentropy(),
    hypergrid = hypergrid,
    objective=&quot;val_accuracy&quot;,
    directory = &#39;kt-dir&#39;,
    callbacks=[
        EarlyStopping(
            monitor=&#39;loss&#39;, 
            patience=5
            )
        ]
    )

model.execute(X_train,y_train,validation_size=.2)</code></pre>
<div id="making-predictions" class="section level6">
<h6>Making predictions</h6>
<p>Now that we have our fitted model, let us evaluate its predictive power. In this context, we are going to use accuracy as our evaluation metric. Accuracy is the portion of classifications which were correct. Typically, this metric is suitable when there are roughly equal numbers of classes in the data and we consider any kind of misclassification as equally undesirable.</p>
<pre class="python"><code>y_pred = model.model.predict(X_test).argmax(axis=1)

accuracy = accuracy_score(y_test.argmax(axis=1),y_pred)
print(&quot;Accuracy of this model is: &quot;+&quot;{:1.2f}&quot;.format(accuracy*100)+&quot;%&quot;)</code></pre>
<pre><code>2/2 [==============================] - 0s 0s/step
Accuracy of this model is: 98.00%</code></pre>
<p>Success!! The best model discovered by our hypertuner was 98% accurate, a big improvement over 70% in our basic model.</p>
</div>
<div id="fitting-history" class="section level6">
<h6>Fitting history</h6>
<p>The history parameter helps us monitor how effectively the fitting process converged on the optimum accuracy. We can construct a timeline which shows us how the accuracy metric improved at each epoch as the optimizer progressively improved the weightings. Note that once a point of diminishing return is reached, the EarlyStopping callback function will conclude the fitting process early to save computation time, which is why we cannot see the requested 150 epochs.</p>
<p>Practitioners should expect to see a steady increase in performance over epochs. A history showing no improvement over epochs would indicate a problem with the model.</p>
<pre class="python"><code>pd.Series(
    model.history.history[&#39;accuracy&#39;]
    ).plot(
        xlabel=&#39;Epochs&#39;,
        ylabel=&quot;Accuracy&quot;
        )</code></pre>
<pre><code>&lt;Axes: xlabel=&#39;Epochs&#39;, ylabel=&#39;Accuracy&#39;&gt;</code></pre>
<p><img src="../../../../../socialsciencedatalab/article/ann/ANN_Tutorial_28_1.png" width="100%" style="display: block; margin: auto;" /></p>
</div>
<div id="what-were-the-best-hyperparameters" class="section level6">
<h6>What were the best hyperparameters?</h6>
<p>The <code>best_hyperparams</code> property will tell us which of the parameters we trialed was best. Note that some of the ‘tuner’ parameters are internal considerations for the tuner and not important to us for now.</p>
<pre class="python"><code>model.best_hyperparams.values</code></pre>
<pre><code>{&#39;units&#39;: 52,
 &#39;layers&#39;: 2,
 &#39;droprate&#39;: 0.1,
 &#39;activation&#39;: &#39;relu&#39;,
 &#39;learning_rate&#39;: 0.01,
 &#39;tuner/epochs&#39;: 2,
 &#39;tuner/initial_epoch&#39;: 0,
 &#39;tuner/bracket&#39;: 4,
 &#39;tuner/round&#39;: 0}</code></pre>
</div>
<div id="what-does-our-model-look-like" class="section level6">
<h6>What does our model look like?</h6>
<p>We can use the <code>summary()</code> function to print out a basic summary of our model’s architecture.</p>
<pre class="python"><code>model.model.summary()</code></pre>
<pre><code>Model: &quot;sequential_1&quot;
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 Dense_0 (Dense)             (None, 52)                260       
                                                                 
 dropout_1 (Dropout)         (None, 52)                0         
                                                                 
 Dense_1 (Dense)             (None, 52)                2756      
                                                                 
 dropout_2 (Dropout)         (None, 52)                0         
                                                                 
 dense_1 (Dense)             (None, 3)                 159       
                                                                 
=================================================================
Total params: 3,175
Trainable params: 3,175
Non-trainable params: 0
_________________________________________________________________</code></pre>
</div>
<div id="evaluating-performance" class="section level6">
<h6>Evaluating Performance</h6>
<p>We know the model was 98% accurate, but let us examine more deeply how well it classified each species. We shall use a confusion matrix.</p>
<pre class="python"><code>matplotlib.rc(&#39;figure&#39;, figsize=(5,5))
ConfusionMatrixDisplay(
    confusion_matrix(
        y_test.argmax(axis=1), 
        y_pred)
        ).plot(
            colorbar=False
            )
plt.title(&#39;Confusion Matrix&#39;)</code></pre>
<pre><code>Text(0.5, 1.0, &#39;Confusion Matrix&#39;)</code></pre>
<p><img src="../../../../../socialsciencedatalab/article/ann/ANN_Tutorial_34_1.png" width="100%" style="display: block; margin: auto;" /></p>
</div>
<div id="interpreting-the-model" class="section level6">
<h6>Interpreting the model</h6>
<p>Most consumers of machine learning models want to know <em>why</em> a model gives the output it does. This can be a difficult task for a complex neural network, with many interactions nested in the algorithm. In a linear model, each feature has a positive or negative impact on the outcome. In an interactive model, the direction and size of effect is conditional and not so simple to describe.
A common method for explaining why models output as they do is ‘permutational feature importance’ in which we calculate the loss of accuracy in a model when it does not have a given feature available. However, this is a lot of repeated computation when you have many features and ANNs can be slow for this.</p>
<p>Another approach is <a href="https://shap.readthedocs.io/en/latest/">SHapley Additive exPlanations (SHAP)</a>, which uses an algorithm based on game-theory as well as many computational optimizations, to efficiently calculate ‘SHAP values’ for an ANN. Here we shall demonstrate SHAP. A larger SHAP value indicates the feature is more influential.</p>
<pre class="python"><code>deep_explain = shap.DeepExplainer(model.model, X_test)
deep_shap_values= deep_explain.shap_values(X_test[1:5])

matplotlib.rc(&#39;figure&#39;, figsize=(10,10))
pd.Series(
    deep_shap_values[0][0],
    index=feature_names
    ).sort_values(
    ).plot.barh(
        title=&#39;SHAP Values&#39;
        )</code></pre>
<pre><code>&lt;Axes: title={&#39;center&#39;: &#39;SHAP Values&#39;}&gt;</code></pre>
<p><img src="../../../../../socialsciencedatalab/article/ann/ANN_Tutorial_36_1.png" width="100%" style="display: block; margin: auto;" /></p>
<p>It appears that petal length and width are the most important factors when trying to classify a flower. Sepal width is also important, but sepal length is relatively inconsequential.</p>
</div>
</div>
</div>
<div id="save-and-load-keras-models" class="section level3">
<h3>Save and load Keras models</h3>
<p>Often, once we have developed our model, we want to deploy it elsewhere so it can used in some other application.
Keras offers a method for saving the model as a directory of files, which can then be sent anywhere you desire.
That directory can then be loaded in python runtime to instantiate a replica of the fitted model. It is ready to make predictions right after loading.</p>
<pre class="python"><code>model.model.save(&#39;keras_model&#39;)
loaded_model = keras.models.load_model(&#39;keras_model&#39;)

loaded_model_y_preds = loaded_model.predict(X_test) &gt;= .5

loaded_model_accuracy = accuracy_score(
    y_test,
    y_pred = loaded_model_y_preds
    )
print(
    &quot;Accuracy of this simple model is: &quot;+
    &quot;{:1.2f}&quot;.format(accuracy*100)+&quot;%&quot;
    )</code></pre>
<pre><code>WARNING:absl:Found untraced functions such as _update_step_xla while saving (showing 1 of 1). These functions will not be directly callable after loading.


2/2 [==============================] - 0s 0s/step
Accuracy of this simple model is: 98.00%</code></pre>
</div>
<div id="other-use-cases" class="section level3">
<h3>Other use cases</h3>
<p>In case you want to reuse this code, but for some other use case, like binary classification or regression, we demonstrate how the same class can easily accomplish those as well.</p>
<div id="binary-classification" class="section level5">
<h5>Binary classification</h5>
<pre class="python"><code>dataset = datasets.load_breast_cancer(
    return_X_y=False, as_frame=False
    )
X = dataset.data
y = dataset.target
y = np_utils.to_categorical(y)
n_classes = len(dataset.target_names) 
input_shape = len(dataset.feature_names)
feature_names = dataset.feature_names 
X_train, X_test, y_train, y_test = train_test_split(
    X, 
    y, 
    test_size=0.33, 
    random_state=42
    )

model = ANNHyperModel(
    input_shape =input_shape,
    n_outputs = n_classes,
    epochs = 150,
    loss = keras.losses.BinaryCrossentropy(),
    hypergrid = hypergrid,
    objective=&quot;val_accuracy&quot;,
    directory = &#39;kt-dir&#39;,
    callbacks=[EarlyStopping(monitor=&#39;loss&#39;, patience=5)]
    )

model.execute(X_train,y_train,validation_size=.2)

y_pred = model.model.predict(X_test).argmax(axis=1)
accuracy = accuracy_score(y_test.argmax(axis=1),y_pred)
print(
    &quot;Accuracy of this model is: &quot;+
    &quot;{:1.2f}&quot;.format(accuracy*100)+&quot;%&quot;
    )</code></pre>
<pre><code>6/6 [==============================] - 0s 0s/step
Accuracy of this model is: 94.15%</code></pre>
</div>
<div id="regression" class="section level5">
<h5>Regression</h5>
<pre class="python"><code>
dataset = datasets.load_diabetes(return_X_y=False)
X = dataset.data
y = dataset.target
input_shape = len(dataset.feature_names)
feature_names = dataset.feature_names 
X_train, X_test, y_train, y_test = train_test_split(
    X, 
    y, 
    test_size=0.33, 
    random_state=42
    )

model = ANNHyperModel(
    input_shape =input_shape,
    ### we are predicting one continuous variable, so we want 
    # only one output node
    n_outputs = 1, 
    epochs = 150,
    loss = keras.losses.MeanAbsoluteError(),
    output_activation=None,
    hypergrid = hypergrid,
    metrics=[
            tf.keras.metrics.MeanSquaredError(
            name=&quot;mean_absolute_error&quot;, 
            dtype=None
            )
        ],
    objective=&quot;val_mean_absolute_error&quot;,
    directory = &#39;kt-dir&#39;,
    callbacks=[EarlyStopping(monitor=&#39;loss&#39;, patience=5)]
    )

model.execute(X_train,y_train,validation_size=.2)

### use flatten because the single output node outputs a 
# series of vectors of 1 item each. Flatten makes 
# 1d array instead
y_pred = model.model.predict(X_test).flatten() 

error = mean_absolute_error(y_test,y_pred)
print(&quot;MAE of this model is: &quot;+&quot;{:1.2f}&quot;.format(error*100))</code></pre>
<pre><code>5/5 [==============================] - 0s 4ms/step
MAE of this model is: 4506.16</code></pre>
</div>
</div>
<div id="conclusion" class="section level3">
<h3>Conclusion</h3>
<p>This article provided a hand-on demonstration of how to implement ANNs for your own context. By following the code in this article, you should be able to adapt the reusable <code>ANNHyperModel</code> class to your own project. ANNs are a powerful machine learning algorithm, and can be applied to many different problems. With a basic understanding of how to implement ANNs, you can also start to progress to learn more advanced types of neural networks, including Recurrent Neural Networks and Convolutional Neural Networks.</p>
</div>
<div id="further-reading" class="section level3">
<h3>Further reading</h3>
<ul>
<li><a href="https://www.youtube.com/watch?v=CqOfi41LfDw&amp;ab_channel=StatQuestwithJoshStarmer">This video tutorial series on the theory of ANNs.</a></li>
<li><a href="https://playground.tensorflow.org">The tensorflow playground</a>, a fun visual aid for intuitively understanding how neural networks function.</li>
<li><a href="https://www.tensorflow.org/">The documentation for Tensorflow.</a></li>
<li><a href="https://shap.readthedocs.io/en/latest/">The documentation for SHAP.</a></li>
</ul>
</div>
<div id="about-the-author" class="section level3">
<h3>About the author</h3>
<p><a href="https://www.mzes.uni-mannheim.de/d7/en/profiles/john-james-collins">John ‘Jack’ Collins</a> <a href="mailto:john.collins@mzes.uni-mannheim.de"><svg aria-hidden="true" role="img" viewBox="0 0 512 512" style="height:1em;width:1em;vertical-align:-0.125em;margin-left:auto;margin-right:auto;font-size:inherit;fill:currentColor;overflow:visible;position:relative;"><path d="M64 112c-8.8 0-16 7.2-16 16v22.1L220.5 291.7c20.7 17 50.4 17 71.1 0L464 150.1V128c0-8.8-7.2-16-16-16H64zM48 212.2V384c0 8.8 7.2 16 16 16H448c8.8 0 16-7.2 16-16V212.2L322 328.8c-38.4 31.5-93.7 31.5-132 0L48 212.2zM0 128C0 92.7 28.7 64 64 64H448c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128z"/></svg></a> <a href="https://jackcollins91.github.io/jcowebsite/"><svg aria-hidden="true" role="img" viewBox="0 0 512 512" style="height:1em;width:1em;vertical-align:-0.125em;margin-left:auto;margin-right:auto;font-size:inherit;fill:currentColor;overflow:visible;position:relative;"><path d="M352 256c0 22.2-1.2 43.6-3.3 64H163.3c-2.2-20.4-3.3-41.8-3.3-64s1.2-43.6 3.3-64H348.7c2.2 20.4 3.3 41.8 3.3 64zm28.8-64H503.9c5.3 20.5 8.1 41.9 8.1 64s-2.8 43.5-8.1 64H380.8c2.1-20.6 3.2-42 3.2-64s-1.1-43.4-3.2-64zm112.6-32H376.7c-10-63.9-29.8-117.4-55.3-151.6c78.3 20.7 142 77.5 171.9 151.6zm-149.1 0H167.7c6.1-36.4 15.5-68.6 27-94.7c10.5-23.6 22.2-40.7 33.5-51.5C239.4 3.2 248.7 0 256 0s16.6 3.2 27.8 13.8c11.3 10.8 23 27.9 33.5 51.5c11.6 26 21 58.2 27 94.7zm-209 0H18.6C48.6 85.9 112.2 29.1 190.6 8.4C165.1 42.6 145.3 96.1 135.3 160zM8.1 192H131.2c-2.1 20.6-3.2 42-3.2 64s1.1 43.4 3.2 64H8.1C2.8 299.5 0 278.1 0 256s2.8-43.5 8.1-64zM194.7 446.6c-11.6-26-20.9-58.2-27-94.6H344.3c-6.1 36.4-15.5 68.6-27 94.6c-10.5 23.6-22.2 40.7-33.5 51.5C272.6 508.8 263.3 512 256 512s-16.6-3.2-27.8-13.8c-11.3-10.8-23-27.9-33.5-51.5zM135.3 352c10 63.9 29.8 117.4 55.3 151.6C112.2 482.9 48.6 426.1 18.6 352H135.3zm358.1 0c-30 74.1-93.6 130.9-171.9 151.6c25.5-34.2 45.2-87.7 55.3-151.6H493.4z"/></svg></a> <a href="https://www.linkedin.com/in/jack-collins-595a53115/"><svg aria-hidden="true" role="img" viewBox="0 0 448 512" style="height:1em;width:0.88em;vertical-align:-0.125em;margin-left:auto;margin-right:auto;font-size:inherit;fill:currentColor;overflow:visible;position:relative;"><path d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"/></svg></a> is a PhD candidate at the Mannheimer Zentrum für Europäische Sozialforschung (MZES). Jack holds a Master’s in Data Science and his research focuses on applying machine learning to survey methodology. Before coming to Mannheim University for PhD, Jack was an IT consultant.</p>
</div>
]]>
      </description>
    </item>
    
    <item>
      <title>Transformer-based language models</title>
      <link>https://socialsciencedatalab.mzes.uni-mannheim.de/video/transformer-models/</link>
      <pubDate>Tue, 30 May 2023 00:00:00 +0100</pubDate>
      
      <guid>https://socialsciencedatalab.mzes.uni-mannheim.de/video/transformer-models/</guid>
      <description><![CDATA[
        <div id="abstract" class="section level5">
<h5>Abstract</h5>
<p>Transformer-based models have recently gained much attention, especially with the release of ChatGPT. Since 2017, deep learning models based on the Transformer architecture have become an important research tool. Their development and application in various fields, including the social sciences, continue to expand. In this talk, we will examine the components that make up these language models and explore how to train state-of-the-art models with HuggingFace for your research. We will also discuss these models’ limitations and open challenges, including open-source availability, the growing need for resources, responsibility, and more.</p>
</div>
<div id="presenter" class="section level5">
<h5>Presenter</h5>
<p>Christopher Klamm <a href="mailto:klamm@uni-mannheim.de"><i class="fa fa-envelope"></i> </a><a href="https://chkla.github.io/gitPage/"><i class="fa fa-globe"></i> </a><a href="https://twitter.com/chklamm"><i class="fa fa-twitter"></i> </a> is an interdisciplinary researcher at the University of Mannheim (Germany) at the Data and Web Science Group working at the intersection of Natural Language Processing and Computational Political Science.</p>
</div>
]]>
      </description>
    </item>
    
    <item>
      <title>BERT and Explainable AI</title>
      <link>https://socialsciencedatalab.mzes.uni-mannheim.de/article/bert-explainable-ai/</link>
      <pubDate>Tue, 28 Mar 2023 01:00:00 +0100</pubDate>
      
      <guid>https://socialsciencedatalab.mzes.uni-mannheim.de/article/bert-explainable-ai/</guid>
      <description><![CDATA[
        </p>
<p>Natural language processing (NLP) is a fascinating field. Popular NLP techniques for understanding (written) human language include next-sentence predictions, translations, text classifications, or sentiment analysis. Such techniques already permeate our everyday lives: What would the world be without services such as Google Translate, DeepL, or the recently released ChatGPT?
While common <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/article/advancing-text-mining/#usequanteda">bag-of-words approaches</a> can often be a valuable approach for NLP, Google’s release of <a href="https://ai.googleblog.com/2018/11/open-sourcing-bert-state-of-art-pre.html">BERT</a> in 2018 revolutionized the possibilities in NLP.
This <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/categories/tutorials/">Methods Bites Tutorial</a> introduces the logic of large language models (LLM) with a special emphasis on BERT. It provides an applied use case from the social sciences, walks readers through explainable artificial intelligence (AI), and explains how we can leverage explainable AI to explain predictions of our models.</p>
<p>While this tutorial targets a broad audience, it requires some basic familiarity with NLP. Here are a few suggestions on how you may want to approach reading this blog post depending on your prior exposure to NLP:</p>
<ul>
<li><strong>New to NLP?</strong> We encourage you to first read <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/article/advancing-text-mining/">this blog post on data mining</a>, which will familiarize you with the basic concepts in NLP, and then continue with this blog post.</li>
<li><strong>New to BERT models?</strong> We invite you to start reading with the <a href="#bert">introduction to BERT</a>, where we also present a use case for <a href="#bert-social-sciences">social science research</a>.</li>
<li><strong>Already know BERT and up for explainable AI?</strong> Go directly to the <a href="#explainable-ai">second part of the blog post</a>, where we cover the basics and dive into explainable AI for BERT.</li>
</ul>
<p>Throughout the post, we rely on frameworks in Python. While the code of this blog post is heavily Python-focused, you can also use it in R. The package <a href="https://rstudio.github.io/reticulate/"><code>reticulate</code></a> allows you to run Python code chunks mixing with R. RStudio published a <a href="https://blogs.rstudio.com/ai/posts/2020-07-30-state-of-the-art-nlp-models-from-r/">blog post</a> on the essential steps to train your BERT model in R (just as a fun fact: We also wrote this post using <a href="https://rstudio.github.io/reticulate/"><code>reticulate</code></a> – and it works like a charm!).
If you are more into Jupyter notebooks and leveraging the power of <a href="https://pypi.org/project/rpy2/"><code>rpy2</code></a>, you can access our <a href="https://colab.research.google.com/drive/1fkQG8Px6Ug69lcexKUrEPHpr_tfo_qCd?usp=sharing#offline=true&amp;sandboxMode=true">Google Colab sandbox here</a>. If you are new to Python and want to get some basics first, have a look at our Social Science Data Lab sessions on introducing Python (<a href="https://socialsciencedatalab.mzes.uni-mannheim.de/video/python-social-science-part-i/">session 1</a> and <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/video/python-social-science-part-ii/">session 2</a>). If you want to stick to plain R, there are also (preliminary) package implementations out there such as <a href="https://github.com/jonathanbratt/RBERT"><code>RBERT</code></a>.</p>
<!-- 
Generate an overview of the article 
Note: Section anchors are generated automatically from section headings as 
      hyphenated lower-case labels without special characters; e.g. a section
      title "Why R?" will result in the anchor "#why-r".
-->
<div id="overview" class="section level3">
<h3>Overview</h3>
<ol style="list-style-type: decimal">
<li><a href="#bert"><strong>BERT</strong></a>
<ol style="list-style-type: decimal">
<li><a href="#bert-bow">Contrasting BERT and the bag-of-words approach</a></li>
<li><a href="#bert-training">How are BERT models trained?</a></li>
<li><a href="#bert-work">How do BERT models “work”?</a></li>
<li><a href="#understanding">Hands-on: Understanding how BERT embeds text</a></li>
<li><a href="#bert-social-sciences">Applying BERT to social science data</a></li>
</ol></li>
<li><a href="#explainable-ai"><strong>Explainable AI</strong></a>
<ol style="list-style-type: decimal">
<li><a href="#background">Background of explainable AI</a></li>
<li><a href="#bert-and-explainable-ai">Explainable AI for for BERT</a></li>
<li><a href="#how-does-it-work">How does “Transformers Interpret” work?</a></li>
<li><a href="#challenges-potentials">Potentials and challenges of explainable AI for research and applied use</a></li>
</ol></li>
<li><a href="#further-reading"><strong>Further reading</strong></a></li>
</ol>
</div>
<div id="bert" class="section level3">
<h3>BERT <a name="bert"></a></h3>
<div id="contrasting-bert-and-the-bag-of-words-approach" class="section level4">
<h4>Contrasting BERT and the bag-of-words approach <a name="bert-bow"></a></h4>
<p>Bag-of-words models are still a frequently used approach to tackle research questions in social science <span class="citation">(<a href="#ref-munger_bonneau_nagler_tucker_2019" role="doc-biblioref">Munger et al. 2019</a>; <a href="#ref-soroka2015bow" role="doc-biblioref">Soroka, Stecula, and Wlezien 2015</a>)</span>. While these approaches revolutionized the capability to work with text by converting text into meaningful representations of numbers, there was still no contextual representation involving the position of certain tokens in an input sequence.</p>
<p>Following the scientific concept of standing on the shoulder of giants, the models and concepts following in the years after more traditional bag-of-words approaches – word embeddings, RNN, (bi-directional) LSTM, and attention-based architectures – subsequently improved the understanding of the text and do not stop at the current state-of-the-art: transformers. BERT models belong to the transformer architecture. The word <em>BERT</em> is the acronym for <strong>b</strong>idirectional <strong>e</strong>ncoder <strong>r</strong>epresentation from <strong>t</strong>ransformers.<a href="#fn1" class="footnote-ref" id="fnref1"><sup>1</sup></a></p>
<p>While BERT is a transformer-based model, it only makes use of one part of the traditional transformer model architecture. <a href="https://huggingface.co/blog/encoder-decoder">Transformers are typically built upon encoders (that translate the sentence into a vector to make it machine-readable) and decoders (that back-translate the sentence again, possibly to another language)</a>. BERT only relies on the encoder part.</p>
<p>Contrasting both the bag-of-words approach and BERT, a major advantage is that BERT comes with a pre-trained language model where you can use your labeled data to fine-tune it. One way of visualizing the difference between both approaches is to think of a student: with bag-of-words, you need to teach the student the language first. With BERT, you have a student who already knows the language but you are teaching the student a specific topic such as biology.</p>
<p>When applying the bag-of-words approach, we train our models based on so-called <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/article/advancing-text-mining/#usequanteda">document-feature matrices in the bag-of-words approach</a>. For this, we use pre-labeled sentences and split them into their single tokens. A token often means a single word in a sentence. We then count how frequently each token occurs in each sentence. Using the analogy drawn above, the model learns (very simply speaking) to relate the number of specific tokens to a specific topic in biology. If a sentence has, for instance, many occurrences of the token “talus” (the Latin term for ankle), it is more likely to belong to anatomy than to neurobiology. The model, however, has neither previous knowledge about the structure of the language nor about biology. It has to learn both concepts in one go and this usually requires a large(r) amount of training data.<a href="#fn2" class="footnote-ref" id="fnref2"><sup>2</sup></a></p>
<p>When using BERT models, in contrast, we already have a pre-trained model at hand that we can fine-tune using pre-labeled data. Fine-tuning describes the phase where we give our pre-trained model task-specific labeled data and seek to improve its classification performance. Similar to the bag-of-words approach, it helps here to have a data set at hand that covers your area of interest and where you manually categorized the sentences based on a set of categories. Showing now BERT these texts throughout the fine-tuning phase is a bit different than we know it from bag-of-words. BERT already knows how the sentences are usually structured as well as which words and sentences often go together. To stick to the analogy with biology used before, we can say that the model already took some English classes and acquired the knowledge before going to the biology class. The model (or the student) has already an understanding of the English language but will learn throughout the fine-tuning phase which sentence covers the topics such as anatomy or neurobiology. The model learns based on the labeled examples provided and is then, with good quality and amount of training data, able to apply the newly gained knowledge to unknown sentences.</p>
<p>Setting up a BERT model training pipeline in general is not so much different from the bag-of-words approach. In the training pipeline, you define the steps that you want to follow – such as pre-processing the data by generating tokens and converting the data into a format that the model can work with and eventually training the model. As you can see in the visualization, the steps are quite similar: You load the data, pre-process them, and finally use them to train or fine-tune the model. If you are interested in more details on how the bag-of-words approach can be implemented in R, you have a look at our blog post on <a href="https://socialsciencedatalab.mzes.uni-mannheim.de/article/advancing-text-mining/">text mining</a>.</p>
<img src="../../../../../article/bert-explainable-ai/img/BERT_BoW.png" width="100%" style="display: block; margin: auto;" />
<details>
<summary>
Alternative text
</summary>
<p>Visualization showing two different workflows (bag-of-words and BERT). The main difference is that with BERT you build upon a pre-trained model and tokenizer while with bag-of-words you often have to train a model from scratch. You can also access the visualization <a href="https://github.com/cosimameyer/illustrations">here</a>.</p>
 
</details>
<p> </p>
</div>
<div id="how-are-bert-models-trained" class="section level4">
<h4>How are BERT models trained? <a name="bert-training"></a></h4>
<!-- Add some text here  -->
<p>To understand the advantage of transformer-based models, we explain the terms of both pre-training and fine-tuning in more detail. These two phases split the model training into two parts.</p>
<p>During the <strong>pre-training phase</strong>, different unsupervised learning tasks are solved on a large amount of textual data to train the model. One of these tasks is called masked language modeling; the other is called next sentence prediction. It is crucial to understand that both of them do not need any prior manual annotation – raw text data is sufficient to train on these tasks. This is what we call unsupervised learning.<a href="#fn3" class="footnote-ref" id="fnref3"><sup>3</sup></a> The procedure initializes the model with the specific characteristics (e.g. vocabulary, grammar, and slang) of the input data which makes it especially interesting for text sources that are rather hard to understand. The next task deals with predicting the next sentence. This way, the model learns which sentences usually follow each other.</p>
<img src="../../../../../article/bert-explainable-ai/img/bert_train.png" width="100%" style="display: block; margin: auto;" />
<details>
<summary>
Alternative text
</summary>
<p>Image showing how BERT models are trained.
The first half of the training involves masking the words (Mask ML). During the training period, you mask one word at a time and the model learns, which word usually follows.
During the second half, you train the model to predict the next sentence. This way, the model learns which sentences usually follow each other. You can also access the visualization <a href="https://github.com/cosimameyer/illustrations">here</a>.</p>
 
</details>
<p> </p>
<p>The so-called <strong>fine-tuning</strong> covers the initialization of the model with the pre-trained parameters as well as the adaption to the task at hand by optimizing these parameters using annotated texts.</p>
</div>
<div id="how-do-bert-models-work" class="section level4">
<h4>How do BERT models “work”?<a name="bert-work"></a></h4>
<p>But how do transformer models and transformer-based models work in detail? As already mentioned, BERT does not process textual tokens in a bag-of-words manner as most common approaches such as <a href="https://alvinntnu.github.io/NTNU_ENC2045_LECTURES/nlp/ml-sklearn-classification.html">Support Vector Machines (SVM), Logistic Regression</a>, or <a href="https://www.tandfonline.com/doi/full/10.1080/19312458.2019.1594741">Wordscores</a> do. Although these algorithms provide promising results in some tasks, the position and importance of single words in a textual phrase have only a limited influence on their estimates. As this is especially crucial for texts with a high semantic share (e.g., social media communication), we need a more sophisticated approach for a deeper understanding. Transformer-based models like BERT take into account the contextual representation of every single word dependent on its surroundings (we will provide more information on what this means in the <a href="#understanding">next section</a>).</p>
<p>To do this, transformer(-based) models do neither process a text sequence word by word from left-to-right, or right-to-left nor see it as the bag-of-words approach. They rather use the context from both sides of a word (and read it as a whole at once). This is what makes them <em>bidirectional</em> and allows them to learn the context of a single word based on its surroundings. By doing that, they respect the order of words and additionally build on word embeddings, which can relate semantically similar words with each other.</p>
<p>An important underlying concept is <a href="https://ai.googleblog.com/2017/08/transformer-novel-neural-network.html">“attention”</a>.<a href="#fn4" class="footnote-ref" id="fnref4"><sup>4</sup></a> It is essential for detecting dependencies between elements and therefore capture a context in the input sequence. It adds different weights according to the importance of a single element. This enables one to put more or less emphasis on one or several words in a text and thus helps the model to act more human-like in its decisions. If you want to know more, there is a <a href="https://www.youtube.com/watch?v=xI0HHN5XKDo">great video explaining how a BERT model works</a>.</p>
<p>If you want to start coding and applying what you have learned so far, there is a fantastic framework in Python to work with BERT models: <a href="https://huggingface.co">🤗 Huggingface</a>. It has several pre-trained models available on the website including detailed tutorials that are very easy to follow.</p>
<p>Once you understand how BERT works, you can also apply the logic to a <a href="https://huggingface.co/tasks">variety of text, audio, or video data tasks</a>. For this blog post, we will use text data as an example.</p>
</div>
<div id="hands-on-understanding-how-bert-embeds-text" class="section level4">
<h4>Hands-on: Understanding how BERT embeds text <a name="understanding"></a></h4>
<p>To understand how a BERT model works, understanding how the model captures text and how it is trained are good starting points.</p>
<p>With BERT, you identify the order of the input. This means that the model first extracts various information on how the text you provide is composed. For this, it uses different layers, which you can also see in the visualization below: <em>token embedding</em>, <em>segment embedding</em>, and <em>position embedding</em>. Using the example phrase “Mannheim is a beautiful city”, you can see how BERT would extract the different layers of information.</p>
<p>With <em>token embedding</em>, BERT captures the single components of your sentence. Tokenization refers to splitting a text into its fragments - usually single words. These fragments are called tokens. As you can see, BERT uses special tokens such as [CLS] and [SEP] to make sense of the input. With <em>segment embedding</em>, the model gets more information about the sentences to which the tokens belong. With <em>position embedding</em>, BERT identifies where each token is placed in the sentence and thus learns the order of the tokens.</p>
<img src="../../../../../article/bert-explainable-ai/img/bert_embed.png" width="100%" style="display: block; margin: auto;" />
<details>
<summary>
Alternative text
</summary>
<p>The visualization shows how BERT understands the text.
With BERT, you identify the order of the input. You give the model information about different embedding layers (the tokens (BERT uses special tokens ([CLS] and [SEP]) to make sense of the sentence), the positional embedding (where each token is placed in the sentence), and the segment embedding (which gives you more info about the sentences to which the tokens belong). You can also access the visualization <a href="https://github.com/cosimameyer/illustrations">here</a>.</p>
 
</details>
<p> </p>
<p>To illustrate these concepts, we use a simple example where we encode two sentences (“Mannheim is a beautiful city. It’s close to two rivers and quite green.”) and generate the output. To do this, we first have to import the two modules <code>AutoModelForSequenceClassification</code> and <code>AutoTokenizer</code> from the <a href="https://pypi.org/project/transformers/"><code>transformers</code> package</a> (which can be installed using the package manager pip in your terminal).<a href="#fn5" class="footnote-ref" id="fnref5"><sup>5</sup></a></p>
<pre class="bash"><code>pip install transformers</code></pre>
<p>To call the model, we define a model name as it appears on the platform <a href="https://huggingface.co/models">HuggingFace</a>: <code>"distilbert-base-uncased-finetuned-sst-2-english"</code>. We will <a href="#pipeline">later dive deeper into the particularities of a distilBERT model</a> and will simply apply it for now. The pre-trained model as well as its tokenizer are loaded with the function <code>from_pretrained</code>.</p>
<pre class="python"><code>from transformers import AutoModelForSequenceClassification, AutoTokenizer
model_name = &quot;distilbert-base-uncased-finetuned-sst-2-english&quot;
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name)</code></pre>
<p>Once we have successfully set up everything, we can move on to our sentence encoding. We lowercase our sample and feed it into the initialized encoder for tokenization and print the result:</p>
<pre class="python"><code>sample = (&quot;Mannheim is a beautiful city. It&#39;s close to two rivers and quite green.&quot;).lower()
encoding = tokenizer.encode(sample)
print(tokenizer.convert_ids_to_tokens(encoding))</code></pre>
<pre><code>[&#39;[CLS]&#39;, &#39;mannheim&#39;, &#39;is&#39;, &#39;a&#39;, &#39;beautiful&#39;, &#39;city&#39;, &#39;.&#39;, &#39;it&#39;, &quot;&#39;&quot;, &#39;s&#39;, &#39;close&#39;, &#39;to&#39;, &#39;two&#39;, &#39;rivers&#39;, &#39;and&#39;, &#39;quite&#39;, &#39;green&#39;, &#39;.&#39;, &#39;[SEP]&#39;]</code></pre>
<p>This output shows us how the tokenizer works and how it adds the special tokens to make sense of the input. It is important to note that the pre-trained model also comes with a pre-trained tokenizer. This means that instead of relying on a generic tokenization approach, we can use a tokenizer that is (ideally) a good fit for our data. If we have, for instance, legal texts, a tokenizer trained on legal terminology may be better suitable to get the right tokens out of our text than a tokenizer trained on internet comments.</p>
<p>As computers work better with numbers than words, the plain tokens are just numbers which represent a unique ID that was generated for each token during the process of pre-training. As already outlined <a href="#bert-training">above</a>, the pre-training is the phase where you train your initial BERT model. We can see the tokens for our example by looking at our encoding output:</p>
<pre class="python"><code>print(encoding)</code></pre>
<pre><code>[101, 25116, 2003, 1037, 3376, 2103, 1012, 2009, 1005, 1055, 2485, 2000, 2048, 5485, 1998, 3243, 2665, 1012, 102]</code></pre>
</div>
<div id="applying-bert-to-social-science-data" class="section level4">
<h4>Applying BERT to social science data <a name="bert-social-sciences"></a></h4>
<p>Throughout this section, we showcase how to apply BERT to social science data. Preparing data for a bag-of-words approach usually requires a bandwidth of pre-processing steps (e.g., stop-word removal or stemming) to prepare the input data. To allow BERT to demonstrate its full capacities, all potential explanatory tokens must remain unaltered. Hence, depending on the pre-trained model, pre-processing only involves lowercasing. However, most tokenizers by default integrate lowercasing as part of their internal routines. Here, we apply this step by hand to show you what the pre-processed tokens look like before evaluation.</p>
<p>As we already know, one can (and has to) choose between many different models trained on different characteristics of data. While this can be overwhelming, the main repository for pre-trained models – <a href="https://huggingface.co">🤗 Huggingface</a> – allows for very specific search queries.</p>
<div id="loading-and-preprocessing-the-united-nations-general-debate-corpus" class="section level5">
<h5>Loading and preprocessing the United Nations General Debate Corpus</h5>
<p>In this tutorial, we will work with the United Nations General Debate Corpus <span class="citation">(<a href="#ref-baturo2017understanding" role="doc-biblioref">Baturo, Dasandi, and Mikhaylov 2017</a>)</span>. It consists of all general debate statements from 1970 (Session 25) to 2020 (Session 75). We store the corpus in a <code>data</code> folder. For demonstration, we will limit it to the last 5 years to predict a sentiment category for each sentence in all speeches using BERT and create a visualization.</p>
<p>To achieve our goal, we include the following steps:</p>
<ul>
<li>Loading the speeches and doing some data wrangling</li>
<li>Setting up and implementing the sentiment prediction pipeline</li>
<li>Iterate over all sentences to predict their sentiment</li>
<li>Visualize the results in a plot</li>
</ul>
<p>If you want to follow the tutorial on your own machine, you can access the data <a href="https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/0TJX8Y">here</a>. If you want to follow the coding examples in a Jupyter notebook, you can access it in <a href="https://colab.research.google.com/drive/1eLX120nIMFOMc11HHErwYGKVE24Iq6sZ?usp=sharing#offline=true&amp;sandboxMode=true">this Google Colab sandbox</a>.</p>
<p>As the data comes within various <code>.txt</code>-files and formats differs slightly over the years, we first have to execute some data processing to bring the data into the required shape:</p>
<pre><code>                                     speech_sentence session  year country
0  Let me begin by congratulating Ms. María Ferna...      73  2018     BRB
1  However, I would like to pause at this stage, ...      73  2018     BRB
2  Those events include the transit of a tropical...      73  2018     BRB
3  Those events are of great concern because the ...      73  2018     BRB
4           I ask myself, what does all that matter?      73  2018     BRB</code></pre>
<details>
<summary>
Code for performing the data processing steps
</summary>
<pre class="python"><code># Load needed packages/functions
import pandas as pd
import glob
import re
import spacy
import os

# Download required spaCy model:
spacy.cli.download(&quot;en_core_web_sm&quot;)

# Function to transform an input raw speech into a sentence-level data frame
def get_speech_text_from_file(filepath):
    # Load the .txt-file
    with open(filepath) as f:
        lines = f.readlines()
    
    # Remove numerical identifiers and obsolete whitespaces
    lines = &quot; &quot;.join([re.sub(r&#39;^\d{1,3}(\.|:)(\t| )|\n&#39;, &#39;&#39;, line) for line in lines])
    # Load spaCy sentencer to split the speech on sentence-level
    nlp = spacy.load(&#39;en_core_web_sm&#39;)
    doc = nlp(lines)
    sentences = [sent.text.strip() for sent in doc.sents]
    
    # Create a data frame to store each sentence
    speech_df = pd.DataFrame(sentences, columns = [&#39;speech_sentence&#39;])
    title_search = re.search(r&#39;Session (\d{1,3}) - (\d{4})/(\w{3})&#39;, filepath)
    
    # Add some meta information
    speech_df[&#39;session&#39;] = title_search.group(1)
    speech_df[&#39;year&#39;] = title_search.group(2)
    speech_df[&#39;country&#39;] = title_search.group(3)
    
    return speech_df

# Data folder path
dir_path = r&#39;./data/&#39;

# List to store files
speeches_df = pd.DataFrame(columns = [&#39;speech_sentence&#39;, &#39;session&#39;, &#39;year&#39;, &#39;country&#39;])

# Identify files in the folder (to loop over relevant once)
folder_in_dir_path = [f.path for f in os.scandir(dir_path) if f.is_dir()]

# Extract the relevant folders
relevant_folders = []

for folder in folder_in_dir_path:
  # Extract the year (it comes with four digits) from the folder name
  year = int(&#39;&#39;.join((str(i)) for i in re.findall(&#39;[0-9]{4}&#39;, folder)))
  # Identify the folder names of the last 5 years
  if year&gt;2015:
    relevant_folders.append(folder)

# Iterate over the directory with relevant folders and crawl files
for folder in relevant_folders:
  for filename in glob.iglob(folder + &#39;/**/*.txt&#39;, recursive=True):
    # Call function to transform an input raw speech into a sentence-level data frame
    speech_df = get_speech_text_from_file(filename)
    # Concatenate all resulting data frames into one
    speeches_df = pd.concat([speeches_df, speech_df], ignore_index=True)

speeches_df.head()</code></pre>
<pre><code>                                     speech_sentence session  year country
0  Let me begin by congratulating Ms. María Ferna...      73  2018     BRB
1  However, I would like to pause at this stage, ...      73  2018     BRB
2  Those events include the transit of a tropical...      73  2018     BRB
3  Those events are of great concern because the ...      73  2018     BRB
4           I ask myself, what does all that matter?      73  2018     BRB</code></pre>
<p>If you are working on a Unix(-like) OS system, you may need to call these commands in your terminal first (whether it’s <code>pip</code> or <code>pip3</code> as well as <code>python</code> or <code>python3</code> depends on your version):</p>
<pre class="python"><code>pip install -U spacy
python3 -m spacy download en_core_web_sm</code></pre>
<p> </p>
</details>
<p> </p>
</div>
<div id="setting-up-and-implementing-the-sentiment-prediction-pipeline" class="section level5">
<h5>Setting up and implementing the sentiment prediction pipeline <a name="pipeline"></a></h5>
<p>Now we have some neatly processed and organized speeches and want to continue with the exciting part: extracting sentiment using BERT and explaining its decisions with a framework. This leads us to an essential question: what is the right model for the data and task?</p>
<p>A good starting point can be the number of downloads of a model and also a well-documented model card. However, there are a few very decisive properties to narrow down the search:</p>
<ul>
<li><strong>Tasks</strong> – What is the underlying task the model is trained on (e.g., text classification or automatic speech recognition)?</li>
<li><strong>Language(s)</strong> – Do the pre-trained models speak the same (or a related) language as the data in my dataset?</li>
<li><strong>Size</strong> – Larger models tend to require more computational power, but also also trained on more data, which is beneficial in some cases.</li>
<li><strong>Source</strong> – Who trained the initial model? Given that these models come in with “prior knowledge”, you usually want to reduce the risk of biases as much as you can. While the selection of the initial training data can give a (first) good hint, using models that are well documented and come from reliable sources (although it can be up for debate how to define this) can be additional selection criteria.</li>
</ul>
<p>BERT is among the most popular transformer-based model. Its architecture is based on twelve of the so-called encoder blocks (as described in the initial BERT paper by <span class="citation">Devlin et al. (<a href="#ref-devlin2018bert" role="doc-biblioref">2018</a>)</span>). As we would like to reduce the computational workload to make this tutorial accessible for everyone, we decided to further work with a <a href="https://huggingface.co/docs/transformers/model_doc/distilbert">distilBERT</a> model: <a href="https://huggingface.co/distilbert-base-uncased-finetuned-sst-2-english"><code>distilbert-base-uncased-finetuned-sst-2-english</code></a>. These models are smaller and faster than BERT-based models. The first distilBERT model was 40% smaller and 60% faster than a base BERT model while retaining about 97% of its functionality <span class="citation">(<a href="#ref-sanh2019distilbert" role="doc-biblioref">Sanh et al. 2019</a>)</span>.<a href="#fn6" class="footnote-ref" id="fnref6"><sup>6</sup></a></p>
<p>Using this model, we first start with our sentiment prediction pipeline. This is usually done by loading an appropriate model and then initializing the tokenizer:</p>
<pre class="python"><code>from transformers import AutoModelForSequenceClassification, AutoTokenizer
from transformers_interpret import SequenceClassificationExplainer

model_name = &#39;distilbert-base-uncased-finetuned-sst-2-english&#39;
model = AutoModelForSequenceClassification.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)</code></pre>
<p>These are also <a href="https://huggingface.co/course/chapter7/3?fw=pt">typical steps</a> you need to do when fine-tuning a BERT model. We skip the fine-tuning step here because we do not have pre-labeled training and test data at hand. This also means that we cannot calculate the common metrics to understand the quality of our fine-tuned models. But with our pre-trained model at hand (we use the <a href="https://huggingface.co/distilbert-base-uncased-finetuned-sst-2-english"><code>distilbert-base-uncased-finetuned-sst-2-english</code></a>), we already have a model that has a good understanding of sentiment in the English language.</p>
<p>For this tutorial, we directly apply the existing knowledge of the model to our text corpus. In the following code chunk, we iterate over all sentences using the Python function <code>apply()</code> to call the custom function <code>predict_sentiment()</code> for each sentence. <code>predict_sentiment()</code> is a function that generalizes the pipeline we already implemented for our toy example in the first part of this blog post. The function takes the sentence and applies tokenization to convert it into a machine-readable format. The so-called inputs are then fed into our pre-trained BERT model to predict. <code>argmax()</code> helps us to find the sentiment class which is assigned with the highest probability according to the prediction. After converting the class ID to its label (either “positive”, “neutral”, or “negative”), we write its result back to a new column called <code>sentiment</code>:</p>
<pre class="python"><code>def predict_sentiment(sequence):
  # Apply tokenization on our input sentence
  inputs = tokenizer(sequence, return_tensors=&#39;pt&#39;)
  # Do the prediction and save the logits
  logits = model(**inputs).logits
  # Find the class (Positive, Neutral, or Negative) which has the highest probability
  predicted_class_id = logits.argmax().item()
  # Return the predicted class
  return(model.config.id2label[predicted_class_id])

# Call predict_sentiment for all sentences
speeches_df[&#39;sentiment&#39;] = speeches_df[&#39;speech_sentence&#39;].apply(lambda speech_sentence: predict_sentiment(speech_sentence))
# The lambda function used here is a Pythonic approach to write an anonymous function 
# and it can take any number of arguments.</code></pre>
<p>We now have the sentiment on the sentence level and we want to visualize some aspects of it. Since we use <a href="https://rstudio.github.io/reticulate/">{<code>reticulate</code>}</a>, it is fairly easy to switch between the worlds of Python and R. In our case, R seems to be a better candidate for visualization (if you love the logic of <a href="https://ggplot2.tidyverse.org"><code>{ggplot2}</code></a>. If you want to stay in Python, you can also give <a href="https://plotnine.readthedocs.io/en/stable/"><code>{plotnine}</code></a> a try).</p>
<details>
<summary>
Code for generating the visualization
</summary>
<pre class="r"><code># Install pacman (package manager) if not done already
# install.packages(pacman)

# Load the tidyverse package via the pacman package manager
pacman::p_load(tidyverse,
               reticulate,
               countrycode)

# Get speeches from Python and transfer them into the R Environment
speeches_df &lt;- py$speeches_df %&gt;%
  # Convert categorical names to sentiment numbers
  dplyr::mutate(
    sentiment = dplyr::case_when(
      sentiment == &quot;NEGATIVE&quot; ~ -1,
      sentiment == &quot;NEUTRAL&quot; ~ 0,
      sentiment == &quot;POSITIVE&quot; ~ 1
    )
  ) %&gt;%
  # group by country and year and calculate net sentiment values by summing them
  dplyr::group_by(country, year) %&gt;%
  dplyr::summarise(net_perc = sum(sentiment))

speeches_df %&gt;%
  # Generate the country name for each country using the
  # `countrycode()` command
  dplyr::mutate(countryname = countrycode(country, &quot;iso3c&quot;, &quot;country.name&quot;)) %&gt;%
  # Filter and only select specific countries that we want to compare
  dplyr::filter(
    countryname %in% c(
      &quot;North Korea&quot;,
      &quot;Germany&quot;,
      &quot;United Kingdom&quot;,
      &quot;United States&quot;,
      &quot;Pakistan&quot;,
      &quot;France&quot;
    )
  ) %&gt;%
  # Now comes the plotting part :-)
  ggplot() +
  # We do a bar plot that has the years on the x-axis and the level of the
  # net-sentiment on the y-axis
  # We also color it so that all the net sentiments greater than 0 get a
  # different color
  geom_col(aes(
    x = year,
    y = net_perc,
    fill = (net_perc &gt; 0)
  )) +
  # Here we define the colors as well as the labels and title of the legend
  scale_fill_manual(
    name = &quot;Sentiment&quot;,
    labels = c(&quot;Negative&quot;, &quot;Positive&quot;),
    values = c(&quot;#C93312&quot;, &quot;#446455&quot;)
  ) +
  # Now we add the axes labels
  xlab(&quot;Time&quot;) +
  ylab(&quot;Net sentiment&quot;) +
  # And do a facet_wrap by country to get a more meaningful visualization
  facet_wrap( ~ countryname) +
  # And make the theme a bit more beautiful
  theme_minimal() + theme(
    strip.background = element_blank(),
    panel.grid.major = element_blank(),
    panel.grid.minor = element_blank()
  )</code></pre>
 
</details>
<img src="../../../../../article/bert-explainable-ai/img/comparison2.png" width="100%" style="display: block; margin: auto;" />
<details>
<summary>
Alternative text
</summary>
<p>The visualization shows the net sentiment in the UN General Assembly speeches of six countries (France, Germany, North Korea, Pakistan, the United Kingdom, and the United States) from 2016-2020. Using color-coded bar graphs we see that France, Germany, and the US are overall positive while the other countries tend to express more negative net sentiments in their speeches.</p>
 
</details>
<p> </p>
<p>Having an understanding of the net sentiment of speeches is a good start. But what we really want is to understand <em>why</em> the model classifies the sentences this way. That is where explainable AI comes in.</p>
</div>
</div>
</div>
<div id="explainable-ai" class="section level3">
<h3>Explainable AI <a name="explainable-ai"></a></h3>
<div id="background-of-explainable-ai" class="section level4">
<h4>Background of explainable AI <a name="background"></a></h4>
<p>Machine learning models are often thought of as “black boxes” where we put in some input and get results out of them without a good understanding of <em>how</em> these models come to a given classification or prediction. This is what explainable AI seeks to change. Explainable (or interpretable) AI is thought of as one core pillar that contributes towards trustworthy AI (which often also includes other aspects, such as ethical AI or <a href="https://ieeexplore.ieee.org/abstract/document/6406467">fair ML</a>). With more and more AI models being used, it is not surprising that we see an increase in interest in these topics <span class="citation">(<a href="#ref-liu2022trustworthy" role="doc-biblioref">Liu et al. 2022</a>; <a href="#ref-markus2021role" role="doc-biblioref">Markus, Kors, and Rijnbeek 2021</a>; <a href="#ref-prasad2020extent" role="doc-biblioref">Prasad et al. 2020</a>; <a href="#ref-romei2012discovering" role="doc-biblioref">Romei, Ruggieri, and Turini 2012</a>; <a href="#ref-Wickramasinghe2020TrustworthyAD" role="doc-biblioref">Wickramasinghe et al. 2020</a>; <a href="#ref-yang2022unbox" role="doc-biblioref">Yang, Ye, and Xia 2022</a>)</span>.</p>
<p>Visualizations are often helpful to better understand machine learning models. Some visualization approaches are model-specific (for instance <a href="https://towardsdatascience.com/kernelshap-vs-treeshap-e00f3b3a27db">tree SHAP for tree-based models</a>), but some are more model-agnostic (for instance, Shapley values, LIME, or ICE plots). The visualization below gives you a brief overview of some of the more well-known approaches. If you are looking for more in-depth explanations, have a look at the book <a href="https://christophm.github.io/interpretable-ml-book/">“Interpretable Machine Learning” by Christoph Molnar</a>.</p>
<p>The examples used in the visualization present toy examples which are only meant to showcase how explainable AI can be used to understand what drives the model prediction. Here, the primary question is what defines a livable city. We can see how (artificial) features such as the number of restaurants or whether there are industries, parks, or cafés can contribute to a city being classified as “livable”.</p>
<img src="../../../../../article/bert-explainable-ai/img/explainable_ai.png" width="100%" style="display: block; margin: auto;" />
<details>
<summary>
Alternative text
</summary>
<p>The visualization of six different model-agnostic approaches to explain machine learning models post-hoc such as</p>
<ul>
<li><strong>Feature importance</strong>: Feature importance is based on the idea of permutation where you shuffle the values of a feature. If this change increases the model error, the feature is perceived to be important.
<strong>Shapley and SHAP value</strong>: SHAP values (SHapley Additive exPlanation) are based on Shapley values — a game theoretical approach that calculates the average of all marginal contributions to all possible outcomes. SHAP approximates this approach and looks at how the prediction changes when we (step-wise) include or exclude each feature. With its additive feature, it also ensures that the sum of all contributing features should equal the difference between the actual prediction and the average prediction.</li>
<li><strong>LIME</strong>: LIME plots tell you locally around a data point what the most important feature is. While they may look similar to SHAP, they are only an approximation (calculated on a small set of features and do not provide a guarantee of accuracy and consistency).</li>
<li><strong>ICE</strong>: ICE plots show the individual conditional expectation where all other features are kept the same and the effects for one feature are calculated.</li>
<li><strong>Partial dependence</strong>: Partial dependence plots visualize the <em>average</em> output of the model for each target feature value for the entire dataset.</li>
<li><strong>Breakdown plot</strong>: Breakdown plots show the contribution of every variable to the final prediction.</li>
</ul>
<p>You can also access the visualization <a href="https://github.com/cosimameyer/illustrations">here</a>.</p>
 
</details>
<p> </p>
</div>
<div id="explainable-ai-for-bert" class="section level4">
<h4>Explainable AI for BERT <a name="bert-and-explainable-ai"></a></h4>
<p>To better understand (and explain) the outcome of BERT models, the Python library <a href="https://github.com/cdpierse/transformers-interpret">Transformers Interpret</a> is an excellent starting point and offers post-hoc explainability. Post-hoc means that you use Transformers Interpret on your classification results. It gives you a good understanding of which word was more likely to contribute to the classified sentiment. But before we get into it, we will explain how Transformers Interpret generally works.<a href="#fn7" class="footnote-ref" id="fnref7"><sup>7</sup></a></p>
</div>
<div id="how-does-transformers-interpret-work" class="section level4">
<h4>How does “Transformers Interpret” work? <a name="how-does-it-work"></a></h4>
<p>Transformers Interpret builds upon the <a href="https://captum.ai">Captum</a> framework, an explainable AI framework for PyTorch-based models. <a href="https://pytorch.org">PyTorch</a> is a common open-source machine learning framework in Python that can be used to build deep learning models.<a href="#fn8" class="footnote-ref" id="fnref8"><sup>8</sup></a> If you are looking for other model types (and want to go beyond text data), have a look at their <a href="https://captum.ai/tutorials/">tutorials</a>.<a href="#fn9" class="footnote-ref" id="fnref9"><sup>9</sup></a></p>
<p>Captum was developed by Facebook AI and presented at the <a href="https://www.youtube.com/watch?v=iVSIFm0UN9I&amp;list=PL_lsbAsL_o2BY-RrqVDKDcywKnuUTp-f3&amp;index=14">PyTorch 2019 Conference</a>. It is a framework that is <a href="https://captum.ai/docs/captum_insights">multi-modal and can be used for any PyTorch-based model</a>, it is extensible and grows over the years with extensions such as <a href="https://github.com/cdpierse/transformers-interpret">Transformers Interpret</a>. Additionally, and for the user probably most important, it is easy to use. To run a post-hoc explanation of a BERT model, you are good to go with only a few lines of code. Captum itself has a <a href="https://captum.ai/docs/algorithms_comparison_matrix">multitude of attribution algorithms</a> that are used to explain what the model does. The library <a href="https://github.com/cdpierse/transformers-interpret">Transformers Interpret</a> uses the idea of <a href="https://distill.pub/2020/attribution-baselines/"><em>Integrated Gradients</em></a> (as well as <em>Layer Integrated Gradients</em> which is a variant of it; see <span class="citation">Janizek, Sturmfels, and Lee (<a href="#ref-janizek2021explaining" role="doc-biblioref">2021</a>)</span> and <span class="citation">Sundararajan, Taly, and Yan (<a href="#ref-sundararajan2017axiomatic" role="doc-biblioref">2017</a>)</span>).<a href="#fn10" class="footnote-ref" id="fnref10"><sup>10</sup></a></p>
<p>Simply speaking, the logic of integrated gradients is as follows:</p>
<p><img src="../../../../../article/bert-explainable-ai/img/integrated_gradients.png" width="100%" style="display: block; margin: auto;" /></p>
<details>
<summary>
Alternative text
</summary>
The visualization shows the logic of integrated gradients. You start with your baseline which does not have any effect on the model classification and continue stepwise using linear interpolation to get to the original input. On the way, you calculate the model’s prediction, compare it to the baseline, and derive the integrated gradients for each input feature by summing up the results of these calculations.
 
</details>
<p> </p>
<p>We start again with our input “Mannheim is a beautiful city” and convert it into a vector representation.<a href="#fn11" class="footnote-ref" id="fnref11"><sup>11</sup></a> We then define a baseline that does not have any effect on the model classification. For text data, this can be a sequence of zeros <span class="citation">(<a href="#ref-sundararajan2017axiomatic" role="doc-biblioref">Sundararajan, Taly, and Yan 2017</a>)</span>. Following the approach by <span class="citation">Sundararajan, Taly, and Yan (<a href="#ref-sundararajan2017axiomatic" role="doc-biblioref">2017</a>)</span>, which introduced the concept of integrated gradients as a method for explainable AI, the algorithm now takes a pre-specified number of steps along a so-called linear interpolation. You can think of it as a line with points on it. Going along this line, we then pick the points on your steps and add them to your initial sequence. This procedure works because for BERT words are not represented as words but as a sequence of numbers (that’s also what we call a vector representation of words). The algorithm calculates the model prediction at each step and compares it to the baseline. The difference in the model’s prediction at each step is then multiplied by the corresponding step size. In the last step, we derive the integrated gradients for each input feature by summing up the results of these calculations. This way, we eventually end up with a sum that indicates how important each input feature is to the prediction (this is also called “attribution score”).<a href="#fn12" class="footnote-ref" id="fnref12"><sup>12</sup></a></p>
<p>Using integrated gradients can be powerful, and what’s best is that their power is not limited to text data but can also be applied to <a href="https://www.tensorflow.org/tutorials/interpretability/integrated_gradients">images and even used for structured data</a>. Most of the explanations you will find online use <a href="https://distill.pub/2020/attribution-baselines/">image data as an example</a>.<a href="#fn13" class="footnote-ref" id="fnref13"><sup>13</sup></a></p>
<p>Now that we covered a basic understanding of how to explain the model’s behavior, we go back to our hands-on use case. But before we see how these algorithms work in practice, we do the logistics and install the required libraries in your terminal.</p>
<pre class="bash"><code>pip install transformers-interpret</code></pre>
<p>Again, depending on your <a href="https://pip.pypa.io/en/stable/installation/">pip version</a>, you might need to call <code>pip</code> or <code>pip3</code> to install the dependencies.</p>
<pre class="python"><code>from transformers_interpret import SequenceClassificationExplainer</code></pre>
<p>With the <code>SequenceClassificationExplainer</code> method, we are now able to explain a sequence classification task such as sentiment classification. It takes both the previously defined model and tokenizer to compute the attributions. Attributions are numeric values that show how positively or negatively a word contributes to the classification. Here, we refer to the previously used <code>model</code> and <code>tokenizer</code>.</p>
<pre class="python"><code>cls_explainer = SequenceClassificationExplainer(model, tokenizer)</code></pre>
<p>Let us first select a sample of five sentences from speeches delivered by a sub-sample of countries:</p>
<pre class="python"><code># Extract five random sentences based on a pre-selection
random_sentences = speeches_df[speeches_df.country.isin([&#39;USA&#39;,&#39;FRA&#39;,&#39;GER&#39;])].sample(n=5,random_state=1234)[&#39;speech_sentence&#39;].reset_index(drop=True)

# Print each sentence in its full length
for sentence in range(len(random_sentences)): 
  print(random_sentences[sentence])</code></pre>
<pre><code>Around the world our message is clear — America&#39;s goal is lasting harmony, and not to go on with these endless wars.

Behind every one of our decisions are the voices and lives of the invisible masses whom we must defend, because we in turn were defended in the past.

That wealth, which rightly belongs to Iran&#39;s people, also goes to shore up Bashar Al-Assad&#39;s dictatorship, fuel Yemen&#39;s civil war and undermine peace throughout the entire Middle East.

Everyone is tempted to follow their own law.

Here too we will remain fully committed.</code></pre>
<div id="using-transformers-interpret-on-a-single-sentence" class="section level5">
<h5>Using Transformers Interpret on a single sentence</h5>
<p>These sentences sound quite diverse – some more positive, some more negative. Let’s check if that is also what the model classified. Using the method <code>cls_explainer</code>, we also generate an object <code>word_attributions</code> that will be helpful later. We use the first sentence as a working example: <code>Around the world our message is clear — America's goal is lasting harmony, and not to go on with these endless wars.</code></p>
<pre class="python"><code>word_attributions = cls_explainer(random_sentences[0])</code></pre>
<p>With our <code>cls_explainer</code> object, we can look at the predicted class:</p>
<pre class="python"><code>cls_explainer.predicted_class_name</code></pre>
<pre><code>&#39;POSITIVE&#39;</code></pre>
<p>And it tells us that the classification is “positive”! But what we are really interested in is which words were important for this classification. This is what you get by calling <code>visualize()</code>:</p>
<pre class="python"><code>cls_explainer.visualize()</code></pre>
<img src="../../../../../article/bert-explainable-ai/img/visualize.png" width="100%" style="display: block; margin: auto;" />
<details>
<summary>
Alternative text
</summary>
<p>The visualization shows the output of the <code>visualize()</code> method. We see in this visualization the predicted label, the attribution label, the overall attribution score, and, most importantly a visual component on the right-hand side. Red highlighted parts contribute negatively to the classification while green highlighted parts contribute positively and white (so no highlight) are considered neutral (or no contribution).</p>
 
</details>
<p> </p>
<p>What we see in this visualization are the predicted label (1), the attribution label (“POSITIVE”), the overall attribution score (0.82, where the magnitude indicates the strength of the contribution), and, most importantly, a visual component on the right-hand side. Red highlighted parts contribute negatively to the classification while green highlighted parts contribute positively and white is considered neutral (or no contribution). The saturation shows the magnitude of the contribution. The visualization also matches with the numeric attributions and shows how positively, negatively, or neutrally a word (feature) contributes to the classification. As mentioned earlier, we can also get them by printing our previously generated <code>word_attributions</code>.</p>
<pre class="python"><code>print(word_attributions)</code></pre>
<pre><code>[(&#39;[CLS]&#39;, 0.0), 
(&#39;around&#39;, 0.027562093028343622), 
(&#39;the&#39;, 0.026963912669903427), 
(&#39;world&#39;, 0.007187272390974884), 
(&#39;our&#39;, 0.04927162987468778), 
(&#39;message&#39;, 0.06561321917173639), 
(&#39;is&#39;, 0.02585681936629481), 
(&#39;clear&#39;, -0.03615942088697932), 
(&#39;—&#39;, 0.2694262447994862), 
(&#39;america&#39;, -0.07749574631815151), 
(&quot;&#39;&quot;, 0.01128585937076096), 
(&#39;s&#39;, -0.006703021887516693), 
(&#39;goal&#39;, -0.07731179666725874), 
(&#39;is&#39;, 0.15398317629444674), 
(&#39;lasting&#39;, 0.13905982954437562), 
(&#39;harmony&#39;, 0.051255688029244316), 
(&#39;,&#39;, -0.00956995279920919), 
(&#39;and&#39;, -0.007379420575587129), 
(&#39;not&#39;, 0.7587632160635064), 
(&#39;to&#39;, 0.20532838096186265), 
(&#39;go&#39;, -0.06103999232141547), 
(&#39;on&#39;, -0.4228297838978442), 
(&#39;with&#39;, -0.11266722350080983), 
(&#39;these&#39;, -0.20836102947117963), 
(&#39;endless&#39;, 0.00042060694659563615), 
(&#39;wars&#39;, 0.05171845681735308), 
(&#39;.&#39;, -0.0031844890266940536), 
(&#39;[SEP]&#39;, 0.0)]</code></pre>
<p>We can also use the output to generate another visualization that gives us an easily accessible overview of the attribution scores. We again use the same color codes as in the previous visualizations. Red means again that the tokens contribute negatively to the classification while the token in green contributes positively. Those in the middle ([SEP] and [CLS] for instance) are categorized as “neutral” (or no contribution) but don’t have any bars because their value is zero.</p>
<details>
<summary>
Code for creating the visualization
</summary>
<pre class="r"><code># Install pacman (package manager) if not done already
# install.packages(pacman)

# Load the tidyverse package via the pacman package manager
pacman::p_load(tidyverse,
               reticulate,
               dplyr,
               data.table)

# Get word_attributes from Python and transfer them into the R Environment
word_attributions &lt;- py$word_attributions

# Now we need to do some pre-processing and extract relevant elements from the list
word_attributions_df &lt;- as.data.frame(rbindlist(word_attributions))

# Visualize the attribution scores for each token using a barplot
word_attributions_df %&gt;%
  # We do some housekeeping first
  rename(token = V1, attribution_scores = V2) %&gt;%
  arrange(token, attribution_scores) %&gt;% 
  ggplot() +
  # We do a bar plot that has the years on the x-axis and the level of the
  # net-sentiment on the y-axis
  # We also color it so that all the attribution scores greater than 0 get a
  # different color
  geom_col(aes(
    x = reorder(token, attribution_scores, sum),
    y = attribution_scores,
    fill = (attribution_scores &gt; 0)
  )) +
  # Reverse the axes
  coord_flip() +
  # Here we define the colors as well as the labels and title of the legend
  scale_fill_manual(
    name = &quot;Attribution Scores&quot;,
    labels = c(&quot;Negative&quot;, &quot;Positive&quot;),
    values = c(&quot;#C93312&quot;, &quot;#446455&quot;)
  ) +
  # Now we add the axes labels
  xlab(&quot;&quot;) +
  ylab(&quot;Attribution Scores&quot;) +
  # And make the theme a bit more beautiful
  theme_minimal() + theme(
      strip.background = element_blank(),
      panel.grid.major = element_blank(),
      panel.grid.minor = element_blank()
  )</code></pre>
 
</details>
<img src="../../../../../article/bert-explainable-ai/img/word_attributions.png" width="100%" style="display: block; margin: auto;" />
<details>
<summary>
Alternative text
</summary>
<p>The visualization shows <code>word_attributions</code>. Again, we use the same color codes as in the previous visualizations. Red means that the tokens contribute negatively while the token in green contributes positively and white is considered neutral (or no contribution).</p>
 
</details>
<p> </p>
<p>This allows us to also have some numerical output next to the visual output. Positive numbers indicate that the feature contributed positively to the classification and negative numbers indicate the opposite. Here we can see from both the numbers and the visualization, that it is mainly the word <em>not</em> that seems to drive the positive classification of the sentence. This is interesting given that a human reader might probably rather go for other words such as <em>harmony</em> or <em>lasting</em>. This can be a first indicator that we need to fine-tune the model. Another interesting pick is the effect of punctuation. The full stop at the end of the sentence seems to negatively contribute to the classification. Here, it would be interesting to see how the model behaves with another punctuation. Understanding how changes in the features can change the model behavior is what explainable AI is for!</p>
</div>
<div id="using-transformers-interpret-on-multiple-sentences" class="section level5">
<h5>Using Transformers Interpret on multiple sentences</h5>
<p>Repeating the steps above for all selected sentences shows us what Transformers Interpret can tell us about the remaining sentences. We can also see the variance of positively and negatively labeled attributes.</p>
<details>
<summary>
Code for creating the visualization
</summary>
<pre class="python"><code># Define a custom function that generates the word_attributions
# and returns the visualization
def interpret_sentence(sentence):
  word_attributions = cls_explainer(sentence)
  return cls_explainer.visualize()

# Here we iterate over the random_sentences and return the 
# visualizations for each sentence
for sentence in range(len(random_sentences)):
    interpret_sentence(random_sentences[sentence])</code></pre>
<p> </p>
</details>
<p><img src="../../../../../article/bert-explainable-ai/img/visualize_all.png" width="100%" style="display: block; margin: auto;" /></p>
<details>
<summary>
Alternative text
</summary>
<p>Output for the <code>visualize()</code> method for the following five sentences:</p>
<ol style="list-style-type: decimal">
<li>Around the world our message is clear – America’s goal is lasting harmony, and not to go on with these endless wars.</li>
<li>Behind every one of our decisions are the voices and lives of the invisible masses whom we must defend, because we in turn were defended in the past.</li>
<li>That wealth, which rightly belongs to Iran’s people, also goes to shore up Bashar Al-Assad’s dictatorship, fuel Yemen’s civil war and undermine peace throughout the entire Middle East.</li>
<li>Everyone is tempted to follow their own law.</li>
<li>Here too we will remain fully committed.</li>
</ol>
<p>We see that, in particular, sentences 2 and 5 are positively attributed while sentences 3 and 4 are negatively attributed.
This visualization shows the predicted label, the attribution label, the overall attribution score, and, most importantly, a visual component on the right-hand side. Red highlighted parts contribute negatively to the classification while green highlighted parts contribute positively and white (so no highlight) are considered neutral (or no contribution).</p>
 
</details>
<p> </p>
<p>We see that – based on the overall attribution score – in particular sentences 2 and 5 are labeled as positive, while sentences 3 and 4 are leaning more towards a negative sentiment. Two interesting takeaways here: First, we see that the magnitude of the attribution score varies across sentences and that the attribution score of the first sentence is rather low in comparison to, for instance, sentence 2. Second, when looking closer at sentence 3, we see that <em>peace</em> and <em>wealth</em> (for instance) are highlighted in red. This informs us as to how these words contributed to the prediction. Since the prediction is “negative” it makes sense that the words <em>peace</em> and <em>wealth</em> do not contribute to the prediction but that it is instead a word like <em>undermine</em> that pushes the model to classify the sentence as “negative”.</p>
</div>
</div>
<div id="potentials-and-challenges-of-explainable-ai-for-research-and-applied-use" class="section level4">
<h4>Potentials and challenges of explainable AI for research and applied use <a name="challenges-potentials"></a></h4>
<div id="using-explainable-ai-for-better-model-understanding-and-fine-tuning" class="section level5">
<h5>Using explainable AI for better model understanding and fine-tuning</h5>
<p>Looking at a few examples (as we just did) is great. But looking at more is even better! This way, we can develop a good understanding of why the model classifies data the way it does and give us an idea of where to improve the training data. Although we can theoretically compute the attribution scores for <em>each</em> input, going through the output requires manual work. In practice, we will be inclined to limit the checks to a sample. In this case, we risk cherry-picking. To avoid this, we should start thinking of a framework for how to best integrate a consistent and systematic explainable AI check in our model evaluation. Depending on the data size, it might be impossible to check all input but we could strategically focus on specific cases. We could, for instance, check a certain percentage of cases falling into a clear positive or negative classification (based on the probabilities) and then, to a larger share, a percentage of those cases falling into a probability area around the threshold. These cases are likely to be somewhat of an “either/or”-decision where it is a great way to learn from explainable AI why the model opted for either a positive or negative classification. These insights will also help us further fine-tune the models (if needed).</p>
</div>
<div id="detecting-biases-and-building-fairer-models" class="section level5">
<h5>Detecting biases and building fair(er) models</h5>
<p>But this is not the only use case of explainable AI. A better model understanding also allows us to detect biases and build fair(er) models. <span class="citation">Hovy and Prabhumoye (<a href="#ref-hovy2021five" role="doc-biblioref">2021</a>)</span> identify <a href="https://compass.onlinelibrary.wiley.com/doi/epdf/10.1111/lnc3.12432">five sources of bias in NLP</a>:</p>
<blockquote>
<p><font size="-1"></p>
<ol style="list-style-type: decimal">
<li>the data,</li>
<li>the annotation process,</li>
<li>the input representations,</li>
<li>the models, and finally,</li>
<li>the research design</li>
</ol>
</font>
</blockquote>
<div style="text-align: right">
<p><sub><sup>
Source: <span class="citation">Hovy and Prabhumoye (<a href="#ref-hovy2021five" role="doc-biblioref">2021</a>)</span>, p. 1
</sub></sup></p>
</div>
<p>Using explainable AI can help us to understand how the model behaves and where potential biases may have been injected. One example was revealed by <a href="https://engineering.princeton.edu/news/2017/04/18/biased-bots-artificial-intelligence-systems-echo-human-prejudices">Arvind Narayanan and Aylin Caliskan</a> who showed that Google Translate tended to <a href="https://ischool.uw.edu/news/2021/08/aylin-caliskan-adds-ischool-expertise-ethical-ai">associate job professions in a gender-biased manner</a>. Using a gender-neutral Turkish pronoun, they showed that professions such as nurse and teacher were associated with women (e.g., “she is a nurse/teacher”) whereas translations of sentences involving professions such as professor or doctor were more likely to be associated with men (e.g., “he is a doctor/professor”).
Thus, when training data are biased, the model is likely to reflect gender-specific biases and stereotypes <span class="citation">(<a href="#ref-caliskan2017semantics" role="doc-biblioref">Caliskan, Bryson, and Narayanan 2017</a>)</span>. Using model explainability can therefore not only benefit practitioners but also general audiences by reducing the reinforcement of gender-specific stereotypes.</p>
<p>But explainable AI is also beneficial beyond text-based models. A prominent example that was portrayed in the Netflix documentary <a href="https://www.netflix.com/de-en/title/81328723">“Coded Bias”</a> highlights the discriminating effects of biased training data in facial recognition. The MIT researcher <a href="https://en.wikipedia.org/wiki/Joy_Buolamwini">Joy Buolamwini</a> discovered that dark-skinned faces are not detected accurately but that the program only worked when she wore a white mask. Becoming aware of these effects and biases is a crucial step in making the world a fair(er) place for everyone.</p>
<p>Another example – while less about fairness – is a situation that you are likely to experience when using <a href="https://en.wikipedia.org/wiki/Reverse_vending_machine">reverse vending machines</a> at the supermarket. <a href="https://www.mdpi.com/2076-3417/11/22/11051">Why do these machines fail to detect your water bottles correctly</a> and, more importantly, what can you, as a user, do about it? Envisioning some improvements, explainable AI could help and visually tell which parts of the bottle were accurately detected and which were not. This could then help us to place the bottles in the correct position when inserting them into the reverse vending machine.</p>
</div>
<div id="using-explainable-ai-as-a-standard-for-rigorous-model-checks" class="section level5">
<h5>Using explainable AI as a standard for rigorous model checks</h5>
<p>As a last potential for explainable AI, we want to draw attention to technical reporting. When we turn to academic papers, we typically report common model metrics such as precision, recall, or F1 scores<a href="#fn14" class="footnote-ref" id="fnref14"><sup>14</sup></a> to show how well our models perform. Digging deeper and doing a more qualitative evaluation of what the models do is needed. With the methods provided (and based on) frameworks such as Captum, this is luckily not too difficult to implement. It helps us to better understand what the models do and why they do it. In scientific papers, we could use this approach, for instance, as part of rigorous model checks, and, if the models are deployed somewhere, having an explainable AI dashboard would be a great asset to monitor and evaluate the model performance beyond the typical model metrics.</p>
<!-- Add a list of further readings -->
</div>
</div>
</div>
<div id="further-reading" class="section level3">
<h3>Further reading <a name="furtherreading"></a></h3>
<ul>
<li><a href="https://github.com/SocialScienceDataLab/fairness-allocation">Achterhold, Eva @ SSDL (2022): Investigating Fairness in Data-Driven Allocation of Public Resources</a></li>
<li><a href="https://blogs.rstudio.com/ai/posts/2020-07-30-state-of-the-art-nlp-models-from-r/">Abdullayev, Turgut (2020): State-of-the-Art NLP Models From R</a></li>
<li><a href="https://ema.drwhy.ai">Biecek, Przemyslaw and Burzykowski, Tomasz (2020): Explanatory Model Analysis</a></li>
<li><a href="https://socialsciencedatalab.mzes.uni-mannheim.de/video/python-social-science-part-i/">Bach, Ruben and Küpfer, Andreas @ SSDL (2023): Getting started with Python (Part I)</a></li>
<li><a href="https://socialsciencedatalab.mzes.uni-mannheim.de/video/python-social-science-part-ii/">Bach, Ruben and Küpfer, Andreas @ SSDL (2023): Getting started with Python (Part II)</a></li>
<li><a href="https://captum.ai/docs/algorithms_comparison_matrix">Captum: Comparison of Algorithms</a></li>
<li><a href="https://github.com/bhoov/exbert">exBERT</a></li>
<li><a href="https://colab.research.google.com/drive/1eLX120nIMFOMc11HHErwYGKVE24Iq6sZ?usp=sharing#offline=true&amp;sandboxMode=true">Google Colab Notebook With the Code</a></li>
<li><a href="https://huggingface.co/tasks">Huggingface: Tasks</a></li>
<li><a href="https://huggingface.co/course/chapter1/1">Huggingface: Tutorial</a></li>
<li><a href="https://socialsciencedatalab.mzes.uni-mannheim.de/article/advancing-text-mining/">Meyer, Cosima and Cornelius Puschmann (2019): Advancing Text Mining with R and quanteda</a></li>
<li><a href="https://github.com/cosimameyer/illustrations">Meyer, Cosima: Data Illustrations</a></li>
<li><a href="https://christophm.github.io/interpretable-ml-book/">Molnar, Christoph (2022): Interpretable Machine Learning - A Guide for Making Black Box Models Explainable</a></li>
<li><a href="https://github.com/rladiescologne/plotnine">Shapiro, Tanya @ R-Ladies Cologne, PyLadies Munich, R-Ladies Paris, PyLadies Tunis (2022): Bringing Your Plots to Cloud Nine With {Plotnine}</a></li>
<li><a href="https://www.tensorflow.org/tutorials/interpretability/integrated_gradients#download_a_pretrained_image_classifier_from_tf-hub">TensorFlow: Integrated Gradients</a></li>
<li><a href="https://github.com/cdpierse/transformers-interpret">Transformers Interpret</a></li>
</ul>
<!-- Add something about the instructor -->
</div>
<div id="about-the-authors" class="section level3">
<h3>About the authors</h3>
<p>Andreas Küpfer <a href="mailto:andreas.kuepfer@tu-darmstadt.de"><i class="fa
              fa-envelope"></i> </a>
<a href="https://andreaskuepfer.github.io"><i class="fa
              fa-globe"></i> </a>
<a href="https://twitter.com/ankuepfer"><i class="fa
              fa-twitter"></i></a> is a computational social scientist and doctoral researcher at the Technical University of Darmstadt. He focuses on the analysis of text, video, and audio and their interplay when it comes to the application of machine learning models.</p>
<p>Cosima Meyer <a href="mailto:contact@cosimameyer.com"><i class="fa
              fa-envelope"></i> </a>
<a href="https://cosimameyer.com/"><i class="fa
              fa-globe"></i> </a>
<a href="https://twitter.com/cosima_meyer"><i class="fa
              fa-twitter"></i></a>
<a href="https://mas.to/@cosima_meyer"><i class="fa
              fa-comments-o"></i> </a> is a data scientist and editor at Methods Bites. She enjoys thinking and learning about how to use trustworthy and explainable AI to create helpful products.</p>
<!-- Optional: Add references (if bib-file is specified) -->
</div>
<div id="references" class="section level3 unnumbered">
<h3>References</h3>
<div id="refs" class="references csl-bib-body hanging-indent">
<div id="ref-baturo2017understanding" class="csl-entry">
Baturo, Alexander, Niheer Dasandi, and Slava J. Mikhaylov. 2017. <span>“Understanding State Preferences with Text as Data: Introducing the UN General Debate Corpus.”</span> <em>Research &amp; Politics</em> 4 (2): 2053168017712821.
</div>
<div id="ref-caliskan2017semantics" class="csl-entry">
Caliskan, Aylin, Joanna J. Bryson, and Arvind Narayanan. 2017. <span>“Semantics Derived Automatically from Language Corpora Contain Human-Like Biases.”</span> <em>Science</em> 356 (6334): 183–86.
</div>
<div id="ref-devlin2018bert" class="csl-entry">
Devlin, Jacob, Ming-Wei Chang, Kenton Lee, and Kristina Toutanova. 2018. <span>“Bert: Pre-Training of Deep Bidirectional Transformers for Language Understanding.”</span> <em>arXiv Preprint arXiv:1810.04805</em>.
</div>
<div id="ref-hovy2021five" class="csl-entry">
Hovy, Dirk, and Shrimai Prabhumoye. 2021. <span>“Five Sources of Bias in Natural Language Processing.”</span> <em>Language and Linguistics Compass</em> 15 (8): 1–19.
</div>
<div id="ref-janizek2021explaining" class="csl-entry">
Janizek, Joseph D., Pascal Sturmfels, and Su-In Lee. 2021. <span>“Explaining Explanations: Axiomatic Feature Interactions for Deep Networks.”</span> <em>Journal of Machine Learning Research</em> 22: 1–54.
</div>
<div id="ref-liu2022trustworthy" class="csl-entry">
Liu, Haochen, Yiqi Wang, Wenqi Fan, Xiaorui Liu, Yaxin Li, Shaili Jain, Yunhao Liu, Anil Jain, and Jiliang Tang. 2022. <span>“Trustworthy Ai: A Computational Perspective.”</span> <em>ACM Transactions on Intelligent Systems and Technology</em> 14 (1): 1–59.
</div>
<div id="ref-markus2021role" class="csl-entry">
Markus, Aniek F., Jan A. Kors, and Peter R. Rijnbeek. 2021. <span>“The Role of Explainability in Creating Trustworthy Artificial Intelligence for Health Care: A Comprehensive Survey of the Terminology, Design Choices, and Evaluation Strategies.”</span> <em>Journal of Biomedical Informatics</em> 113: 103655.
</div>
<div id="ref-munger_bonneau_nagler_tucker_2019" class="csl-entry">
Munger, Kevin, Richard Bonneau, Jonathan Nagler, and Joshua A. Tucker. 2019. <span>“Elites Tweet to Get Feet Off the Streets: Measuring Regime Social Media Strategies During Protest.”</span> <em>Political Science Research and Methods</em> 7 (4): 815–34. <a href="https://doi.org/10.1017/psrm.2018.3">https://doi.org/10.1017/psrm.2018.3</a>.
</div>
<div id="ref-prasad2020extent" class="csl-entry">
Prasad, Grusha, Yixin Nie, Mohit Bansal, Robin Jia, Douwe Kiela, and Adina Williams. 2020. <span>“To What Extent Do Human Explanations of Model Behavior Align with Actual Model Behavior?”</span> <em>arXiv Preprint arXiv:2012.13354</em>.
</div>
<div id="ref-romei2012discovering" class="csl-entry">
Romei, Andrea, Salvatore Ruggieri, and Franco Turini. 2012. <span>“Discovering Gender Discrimination in Project Funding.”</span> In <em>2012 IEEE 12th International Conference on Data Mining Workshops</em>, 394–401.
</div>
<div id="ref-sanh2019distilbert" class="csl-entry">
Sanh, Victor, Lysandre Debut, Julien Chaumond, and Thomas Wolf. 2019. <span>“DistilBERT, a Distilled Version of BERT: Smaller, Faster, Cheaper and Lighter.”</span> <em>arXiv Preprint arXiv:1910.01108</em>.
</div>
<div id="ref-sanyal2021discretized" class="csl-entry">
Sanyal, Soumya, and Xiang Ren. 2021. <span>“Discretized Integrated Gradients for Explaining Language Models.”</span> <em>arXiv Preprint arXiv:2108.13654</em>.
</div>
<div id="ref-soroka2015bow" class="csl-entry">
Soroka, Stuart N., Dominik A. Stecula, and Christopher Wlezien. 2015. <span>“It’s (Change in) the (Future) Economy, Stupid: Economic Indicators, the Media, and Public Opinion.”</span> <em>American Journal of Political Science</em> 59 (2): 457–74. <a href="http://www.jstor.org/stable/24363577">http://www.jstor.org/stable/24363577</a>.
</div>
<div id="ref-sundararajan2017axiomatic" class="csl-entry">
Sundararajan, Mukund, Ankur Taly, and Qiqi Yan. 2017. <span>“Axiomatic Attribution for Deep Networks.”</span> In <em>International Conference on Machine Learning</em>, 3319–28.
</div>
<div id="ref-vaswani2017attention" class="csl-entry">
Vaswani, Ashish, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez, Łukasz Kaiser, and Illia Polosukhin. 2017. <span>“Attention Is All You Need.”</span> <em>Advances in Neural Information Processing Systems</em> 30.
</div>
<div id="ref-wang2020gradient" class="csl-entry">
Wang, Junlin, Jens Tuyls, Eric Wallace, and Sameer Singh. 2020. <span>“Gradient-Based Analysis of NLP Models Is Manipulable.”</span> <em>arXiv Preprint arXiv:2010.05419</em>.
</div>
<div id="ref-Wickramasinghe2020TrustworthyAD" class="csl-entry">
Wickramasinghe, Chathurika S., Daniel L. Marino, Javier Grandio, and Milos Manic. 2020. <span>“Trustworthy AI Development Guidelines for Human System Interaction.”</span> <em>2020 13th International Conference on Human System Interaction (HSI)</em>, 130–36.
</div>
<div id="ref-yang2022unbox" class="csl-entry">
Yang, Guang, Qinghao Ye, and Jun Xia. 2022. <span>“Unbox the Black-Box for the Medical Explainable AI via Multi-Modal and Multi-Centre Data Fusion: A Mini-Review, Two Showcases and Beyond.”</span> <em>Information Fusion</em> 77: 29–52.
</div>
</div>
</div>
<div class="footnotes footnotes-end-of-document">
<hr />
<ol>
<li id="fn1"><p>If you are looking for a concise overview of how BERT models are positioned within other large language models, <a href="https://h2o.ai/wiki/bert/">H2O has something for you</a>.<a href="#fnref1" class="footnote-back">↩︎</a></p></li>
<li id="fn2"><p>Larger datasets are necessary to not only learn more sophisticated data patterns but also split the data into separate train, validation, and test sets, as is typically done in machine learning. Data splitting provides a more comprehensive approach for tuning the model, validating model performance and serving as a robustness test. This is crucial to ensure the accuracy and reliability of the analysis results. Traditional analyses in social science often rely on the entire data set, i.e. do not split between train (validation) and test data. This is a plausible approach when facing limited data but may be worth rethinking when having the benefits of larger data sets. The split, which is common in machine learning, allows the researcher then to specifically validate the performance of the model and can thereby serve as a robustness test.<a href="#fnref2" class="footnote-back">↩︎</a></p></li>
<li id="fn3"><p>The opposite of unsupervised learning is supervised learning. Here we provide the model with labeled data (for instance multiple sentences that are labeled as being “positive” or “negative”). For pre-training a BERT model, raw data are sufficient. In this step, the model acquires an understanding of the language and learns, for instance, which words often go together in specific contexts.<a href="#fnref3" class="footnote-back">↩︎</a></p></li>
<li id="fn4"><p>As a side note: the famous paper “Attention Is All You Need” that introduced the novel architecture of transformer models uses the word “attention” as a pun in its name <span class="citation">(<a href="#ref-vaswani2017attention" role="doc-biblioref">Vaswani et al. 2017</a>)</span>.<a href="#fnref4" class="footnote-back">↩︎</a></p></li>
<li id="fn5"><p>Depending on your <a href="https://pip.pypa.io/en/stable/installation/">pip version</a>, you might need to call <code>pip</code> or <code>pip3</code> to install the dependencies.<a href="#fnref5" class="footnote-back">↩︎</a></p></li>
<li id="fn6"><p>This is achieved by only using six encoder blocks instead of twelve. Additionally, some internal processing (for instance token-type embeddings) are not included in distilBERT.<a href="#fnref6" class="footnote-back">↩︎</a></p></li>
<li id="fn7"><p>Besides Transformers Interpret, there is another library called <a href="https://arxiv.org/abs/1910.05276">exBERT</a>. With exBERT, you get an interactive application that allows you to better understand the <a href="https://www.youtube.com/watch?v=Dtw1lcFg3Ss">contextual representation</a> and what the model has learned when it comes to representation.#<a href="#fnref7" class="footnote-back">↩︎</a></p></li>
<li id="fn8"><p>Besides PyTorch, there are more machine learning frameworks in Python. Another alternative is <a href="https://www.tensorflow.org">TensorFlow</a> with its high-level keras API which was developed by Google. If you are an R user, you may have come across <a href="https://tensorflow.rstudio.com">tensorflow</a>, <a href="https://cran.r-project.org/web/packages/keras/index.html">keras</a> <a href="https://torch.mlverse.org">{torch}</a> which allow you to use these tools in R.<a href="#fnref8" class="footnote-back">↩︎</a></p></li>
<li id="fn9"><p>Taking a step further and looking into the documentation of Captum, we see that we can go beyond the attributions and <a href="https://captum.ai/tutorials/Bert_SQUAD_Interpret">try to interpret BERT’s layers</a>.<a href="#fnref9" class="footnote-back">↩︎</a></p></li>
<li id="fn10"><p>A note of caution here: these algorithms are no panacea and there is also a risk of manipulation as research shows <span class="citation">(<a href="#ref-wang2020gradient" role="doc-biblioref">Wang et al. 2020</a>)</span>.<a href="#fnref10" class="footnote-back">↩︎</a></p></li>
<li id="fn11"><p>For those less familiar with Mannheim: the building that you see is Mannheim’s water tower (or “Wasserturm”), one of its landmarks surrounded by a beautiful small park.<a href="#fnref11" class="footnote-back">↩︎</a></p></li>
<li id="fn12"><p>Since for text data linear interpolation can be quite complex to capture, there are also discretized integrated gradients that take non-linear interpolation into account. So instead of drawing a linear line between the steps, it draws a non-linear line and introduces variations of words (for instance, “bad”, “good”, and “perfect”) to understand what the model predicts. The concept is nicely described in the paper by <span class="citation">Sanyal and Ren (<a href="#ref-sanyal2021discretized" role="doc-biblioref">2021</a>)</span>.<a href="#fnref12" class="footnote-back">↩︎</a></p></li>
<li id="fn13"><p>The <a href="https://www.tensorflow.org/tutorials/interpretability/integrated_gradients#interpolate_images">TensorFlow blog post</a> nicely describes this method using images where it’s like adding more saturation to the image, for instance.<a href="#fnref13" class="footnote-back">↩︎</a></p></li>
<li id="fn14"><p>A detailed explanation of how precision, recall, and F1 score can be calculated and why they are useful can be found <a href="http://iamirmasoud.com/2022/06/19/understanding-micro-macro-and-weighted-averages-for-scikit-learn-metrics-in-multi-class-classification-with-example/">here</a>.<a href="#fnref14" class="footnote-back">↩︎</a></p></li>
</ol>
</div>
]]>
      </description>
    </item>
    
    <item>
      <title>Getting started with Python (Part II)</title>
      <link>https://socialsciencedatalab.mzes.uni-mannheim.de/video/python-social-science-part-ii/</link>
      <pubDate>Tue, 28 Feb 2023 00:00:00 +0100</pubDate>
      
      <guid>https://socialsciencedatalab.mzes.uni-mannheim.de/video/python-social-science-part-ii/</guid>
      <description><![CDATA[
        <div id="abstract" class="section level5">
<h5>Abstract</h5>
<p>The merits of Python for social scientists become tangible when working on a concrete use case. In this follow-up event of our Social Science Data Lab workshop series on Python we use Jupyter Notebooks in the Google Colab environment to implement a simple machine learning routine for prediction. To do that, we first take a step-by-step look at the peculiarities of Python such as data wrangling and basic visualization techniques. With that knowledge, we delve into the basics of applied machine learning by implementing the pipeline for both a logistic regression as well as a random forest model using the Python package scikit-learn. We conclude this workshop with a brief outlook on more advanced possibilities with Python to lay the foundation for your own research.</p>
</div>
<div id="presenters" class="section level5">
<h5>Presenters</h5>
<p>Andreas Küpfer <a href="mailto:akuepfer@mail.uni-mannheim.de"><i class="fa fa-envelope"></i> </a><a href="https://andreaskuepfer.github.io/"><i class="fa fa-globe"></i> </a><a href="https://twitter.com/ankuepfer"><i class="fa fa-twitter"></i> </a> is a doctoral researcher at the University of Darmstadt. His interdisciplinary research interests include text as data, applying machine learning technologies, and substantial inference in the fields of political communication and political competition.</p>
<p>Ruben Bach <a href="mailto:r.bach@uni-mannheim.de"><i class="fa fa-envelope"></i> </a><a href="https://www.sowi.uni-mannheim.de/kreuter/team/academic-staff-members/bach-ruben/"><i class="fa fa-globe"></i> </a><a href="https://twitter.com/rub3n_luc"><i class="fa fa-twitter"></i> </a> is a postdoctoral researcher at the MZES, University of Mannheim, focusing on social science quantitative research methods. His interests include topics related to big data in the social sciences, machine learning, causal inference, and survey research.</p>
</div>
]]>
      </description>
    </item>
    
    <item>
      <title>Getting started with Python (Part I)</title>
      <link>https://socialsciencedatalab.mzes.uni-mannheim.de/video/python-social-science-part-i/</link>
      <pubDate>Tue, 21 Feb 2023 00:00:00 +0100</pubDate>
      
      <guid>https://socialsciencedatalab.mzes.uni-mannheim.de/video/python-social-science-part-i/</guid>
      <description><![CDATA[
        <div id="abstract" class="section level5">
<h5>Abstract</h5>
<p>Other than with R, getting started with Python can be burdensome at times as there is no one-stop shop solution like RStudio. Although tons of introductory tutorials for Python are available on the web, navigating and setting up one’s programming environment can be challenging, especially for users with little programming experience. <!--more-->To lower the burden of getting started with Python, we will talk in this workshop about the basics of Python, installing and maintaining virtual environments and the various graphical user interfaces and integrated development environments out there like Jupyter Notebooks, Google Colab, and Anaconda. We show situations where Python may be beneficial for your research and when you may choose to go with R. Please note that this talk is the first part of a two-day workshop in the Social Science Data Lab. In the second event (February 22, 2023), we will focus our attention on implementing a simple machine learning routine in Python.</p>
</div>
<div id="presenters" class="section level5">
<h5>Presenters</h5>
<p>Ruben Bach <a href="mailto:r.bach@uni-mannheim.de"><i class="fa fa-envelope"></i> </a><a href="https://www.sowi.uni-mannheim.de/kreuter/team/academic-staff-members/bach-ruben/"><i class="fa fa-globe"></i> </a><a href="https://twitter.com/rub3n_luc"><i class="fa fa-twitter"></i> </a> is a postdoctoral researcher at the MZES, University of Mannheim, focusing on social science quantitative research methods. His interests include topics related to big data in the social sciences, machine learning, causal inference, and survey research.</p>
<p>Andreas Küpfer <a href="mailto:akuepfer@mail.uni-mannheim.de"><i class="fa fa-envelope"></i> </a><a href="https://andreaskuepfer.github.io/"><i class="fa fa-globe"></i> </a><a href="https://twitter.com/ankuepfer"><i class="fa fa-twitter"></i> </a> is a doctoral researcher at the University of Darmstadt. His interdisciplinary research interests include text as data, applying machine learning technologies, and substantial inference in the fields of political communication and political competition.</p>
</div>
]]>
      </description>
    </item>
    
  </channel>
</rss>