<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Ryan Rudes on Medium]]></title>
        <description><![CDATA[Stories by Ryan Rudes on Medium]]></description>
        <link>https://medium.com/@ryanrudes?source=rss-7b60671f7b73------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*YbQyX3DQOyYhzsDezm_Lvg.png</url>
            <title>Stories by Ryan Rudes on Medium</title>
            <link>https://medium.com/@ryanrudes?source=rss-7b60671f7b73------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 07 Apr 2026 00:23:40 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@ryanrudes/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Rendering OpenAI Gym Environments in Google Colab]]></title>
            <link>https://medium.com/analytics-vidhya/rendering-openai-gym-environments-in-google-colab-9df4e7d6f99f?source=rss-7b60671f7b73------2</link>
            <guid isPermaLink="false">https://medium.com/p/9df4e7d6f99f</guid>
            <category><![CDATA[openai-gym]]></category>
            <category><![CDATA[openai]]></category>
            <category><![CDATA[google-colab]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[reinforcement-learning]]></category>
            <dc:creator><![CDATA[Ryan Rudes]]></dc:creator>
            <pubDate>Mon, 08 Feb 2021 16:31:11 GMT</pubDate>
            <atom:updated>2023-12-21T00:03:23.620Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zVwt85az4kZEWPnFq-5ReA.jpeg" /><figcaption>Rendering Breakout-v0 in Google Colab with colabgymrender</figcaption></figure><p><strong>UPDATE</strong>: This package has been updated for compatibility with the new gymnasium library and is now called renderlab. Get it <a href="https://github.com/ryanrudes/renderlab">here</a>.</p><p>I’ve released a module for rendering your <a href="https://gym.openai.com/envs/#classic_control">gym</a> environments in <a href="https://colab.research.google.com/">Google Colab</a>. Since Colab runs on a VM instance, which doesn’t include any sort of a display, rendering in the notebook is difficult. After looking through the various approaches, I found that using the <a href="https://pypi.org/project/moviepy/">moviepy</a> library was best for rendering video in Colab. So I built a wrapper class for this purpose, called <a href="https://github.com/Ryan-Rudes/colabgymrender">colabgymrender</a>.</p><h3>Installation</h3><pre>apt-get install -y xvfb python-opengl ffmpeg &gt; /dev/null 2&gt;&amp;1<br>pip install -U colabgymrender</pre><h3>Example</h3><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/85ba1fb1653e1915b03e647a5878ca93/href">https://medium.com/media/85ba1fb1653e1915b03e647a5878ca93/href</a></iframe><h3>Output</h3><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Fnv2dU_9oZJ0%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dnv2dU_9oZJ0&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2Fnv2dU_9oZJ0%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/2cae3d32695106716e9a2d062e69a8a2/href">https://medium.com/media/2cae3d32695106716e9a2d062e69a8a2/href</a></iframe><h3>Usage</h3><p>Wrap a gym environment in the Recorder object.</p><pre>env = gym.make(&quot;CartPole-v0&quot;)<br>env = Recorder(env, &lt;directory&gt;, &lt;fps&gt;)</pre><p>If you specify a frame rate via &lt;fps&gt;, the videos released to &lt;directory&gt; will be of that frame rate. Otherwise, the environment will check for the default frame rate specified by the environment itself in env.metadata[&#39;video.frames_per_second&#39;]. If neither is found, the frame rate will default to 30.</p><p>You can pause or resume recording with env.pause() and env.resume(), but make sure to env.resume() to record for at least one frame before calling env.play(). Otherwise, you’ll get an error for trying to play a video in which no frames were recorded.</p><p>While recording, each time the environment reaches a terminal state, the videos will automatically be released to &lt;directory&gt;. There is no need to manually release recordings. This provides for ease of use without having to manually control the operations of the recorder. Simply record an episode (or part of an episode), then play the content.</p><h3>More Examples</h3><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Fm_S206-IV_Y%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dm_S206-IV_Y&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2Fm_S206-IV_Y%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/901eac865e73186edb41d4f159915a10/href">https://medium.com/media/901eac865e73186edb41d4f159915a10/href</a></iframe><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2F3GzP4oUtU1g%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D3GzP4oUtU1g&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2F3GzP4oUtU1g%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/dc9b7802544d5c511169bc56f022a448/href">https://medium.com/media/dc9b7802544d5c511169bc56f022a448/href</a></iframe><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Fma4Oj775jo0%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dma4Oj775jo0&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2Fma4Oj775jo0%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/b7de4525dede2dfffbf34a5caaca8ae1/href">https://medium.com/media/b7de4525dede2dfffbf34a5caaca8ae1/href</a></iframe><p>Links:</p><ul><li><a href="https://github.com/Ryan-Rudes/colabgymrender/tree/v1.0.3">GitHub</a></li><li><a href="https://pypi.org/project/colabgymrender/">PyPi</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9df4e7d6f99f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/analytics-vidhya/rendering-openai-gym-environments-in-google-colab-9df4e7d6f99f">Rendering OpenAI Gym Environments in Google Colab</a> was originally published in <a href="https://medium.com/analytics-vidhya">Analytics Vidhya</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Controlling a Mouse With Your Eyes]]></title>
            <link>https://medium.com/data-science/controlling-a-mouse-with-your-eyes-f1097e7cf2e9?source=rss-7b60671f7b73------2</link>
            <guid isPermaLink="false">https://medium.com/p/f1097e7cf2e9</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[computer-vision]]></category>
            <category><![CDATA[humancomputer-interaction]]></category>
            <dc:creator><![CDATA[Ryan Rudes]]></dc:creator>
            <pubDate>Mon, 21 Sep 2020 14:38:19 GMT</pubDate>
            <atom:updated>2020-09-21T15:11:58.654Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*15ZuFhSoN1JLG5zWJCf73g.png" /><figcaption>Mouse automatically navigating to a coordinate according to eye position (Image by author)</figcaption></figure><h4>A Machine Learning approach to eye pose estimation from just a single front-facing perspective as input</h4><p>In this project, we’ll write code to crop images of your eyes each time you click the mouse. Using this data, we can train a model in reverse, predicting the position of the mouse from your eyes.</p><p>We’ll need a few libraries</p><pre># For monitoring web camera and performing image minipulations<br>import cv2</pre><pre># For performing array operations<br>import numpy as np</pre><pre># For creating and removing directories<br>import os<br>import shutil</pre><pre># For recognizing and performing actions on mouse presses<br>from pynput.mouse import Listener</pre><p>Let’s first learn how pynput’s Listener works.</p><p>pynput.mouse.Listener creates a background thread that records mouse movements and mouse clicks. Here’s a simplifier code that, upon a mouse press, prints the coordinates of the mouse:</p><pre>from pynput.mouse import Listener</pre><pre>def on_click(x, y, button, pressed):<br>  &quot;&quot;&quot;<br>  Args:<br>    x: the x-coordinate of the mouse<br>    y: the y-coordinate of the mouse<br>    button: 1 or 0, depending on right-click or left-click<br>    pressed: 1 or 0, whether the mouse was pressed or released<br>  &quot;&quot;&quot;<br>  if pressed:<br>    print (x, y)</pre><pre>with Listener(on_click = on_click) as listener:<br>  listener.join()</pre><p>Now, let’s expand this framework for our purposes. However, we first need to write the code that crops the bounding box of your eyes. We’ll call this function from within the on_click function later.</p><p>We use <a href="https://docs.opencv.org/3.4/db/d28/tutorial_cascade_classifier.html">Haar cascade object detection</a> to determine the bounding box of the user’s eyes. You can download the detector file <a href="https://raw.githubusercontent.com/opencv/opencv/master/data/haarcascades/haarcascade_eye.xml">here</a>. Let’s make a simple demonstration to show how this works:</p><pre>import cv2</pre><pre># Load the cascade classifier detection object<br>cascade = cv2.CascadeClassifier(&quot;haarcascade_eye.xml&quot;)</pre><pre># Turn on the web camera<br>video_capture = cv2.VideoCapture(0)</pre><pre># Read data from the web camera (get the frame)<br>_, frame = video_capture.read()</pre><pre># Convert the image to grayscale<br>gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)</pre><pre># Predict the bounding box of the eyes<br>boxes = cascade.detectMultiScale(gray, 1.3, 10)</pre><pre># Filter out images taken from a bad angle with errors<br># We want to make sure both eyes were detected, and nothing else<br>if len(boxes) == 2:<br>  eyes = []<br>  for box in boxes:<br>    # Get the rectangle parameters for the detected eye<br>    x, y, w, h = box<br>    # Crop the bounding box from the frame<br>    eye = frame[y:y + h, x:x + w]<br>    # Resize the crop to 32x32<br>    eye = cv2.resize(eye, (32, 32))<br>    # Normalize<br>    eye = (eye - eye.min()) / (eye.max() - eye.min())<br>    # Further crop to just around the eyeball<br>    eye = eye[10:-10, 5:-5]<br>    # Scale between [0, 255] and convert to int datatype<br>    eye = (eye * 255).astype(np.uint8)<br>    # Add the current eye to the list of 2 eyes<br>    eyes.append(eye)</pre><pre>  # Concatenate the two eye images into one<br>  eyes = np.hstack(eyes)</pre><p>Now, let’s use this knowledge to write a function for cropping the eye image. First, we’ll need a helper function for normalization:</p><pre>def normalize(x):<br>  minn, maxx = x.min(), x.max()<br>  return (x - minn) / (maxx - minn)</pre><p>Here’s our eye cropping function. It returns the image if the eyes were found. Otherwise, it returns None:</p><pre>def scan(image_size=(32, 32)):<br>  _, frame = video_capture.read()</pre><pre>  gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)<br>  boxes = cascade.detectMultiScale(gray, 1.3, 10)</pre><pre>  if len(boxes) == 2:<br>    eyes = []<br>    for box in boxes:<br>      x, y, w, h = box<br>      eye = frame[y:y + h, x:x + w]<br>      eye = cv2.resize(eye, image_size)<br>      eye = normalize(eye)<br>      eye = eye[10:-10, 5:-5]<br>      eyes.append(eye)</pre><pre>    return (np.hstack(eyes) * 255).astype(np.uint8)<br>  else:<br>    return None</pre><p>Now, let’s write our automation, which will run each time we press the mouse button. (assume we have already defined the variable root previously in our code as the directory where we would like to store the images):</p><pre>def on_click(x, y, button, pressed):<br>  # If the action was a mouse PRESS (not a RELEASE)<br>  if pressed:<br>    # Crop the eyes<br>    eyes = scan()<br>    # If the function returned None, something went wrong<br>    if not eyes is None:<br>      # Save the image<br>      filename = root + &quot;{} {} {}.jpeg&quot;.format(x, y, button)<br>      cv2.imwrite(filename, eyes)</pre><p>Now, we can recall our implementation of pynput’s Listener, and make our full code implementation:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/637e8ebfdf882fc583c652d7deaedde3/href">https://medium.com/media/637e8ebfdf882fc583c652d7deaedde3/href</a></iframe><p>When we run this, each time we click the mouse (if both of our eyes are in view), it will automatically crop the webcam and save the image to the appropriate directory. The filename of the image will contain the mouse coordinate information, as well as whether it was a right or left click.</p><p>Here’s an example image. In this image, I am performing a left-click at coordinate (385, 686) on a monitor with resolution 2560x1440:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/440/1*J_9_Xt04Y1bQFlvPCo3Bfg.jpeg" /><figcaption>An example (Image by author)</figcaption></figure><p>The cascade classifier is highly accurate, and I have not seen any mistakes in my own data directory so far.</p><p>Now, let’s write the code for training a neural network to predict the mouse position, given the image of your eyes.</p><p>Let’s import some libraries</p><pre>import numpy as np<br>import os<br>import cv2<br>import pyautogui</pre><pre>from tensorflow.keras.models import *<br>from tensorflow.keras.layers import *<br>from tensorflow.keras.optimizers import *</pre><p>Now, let’s add our cascade classifier:</p><pre>cascade = cv2.CascadeClassifier(&quot;haarcascade_eye.xml&quot;)<br>video_capture = cv2.VideoCapture(0)</pre><p>Let’s add our helper functions.</p><p>Normalization:</p><pre>def normalize(x):<br>  minn, maxx = x.min(), x.max()<br>  return (x - minn) / (maxx - minn)</pre><p>Capturing the eyes:</p><pre>def scan(image_size=(32, 32)):<br>  _, frame = video_capture.read()</pre><pre>  gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)<br>  boxes = cascade.detectMultiScale(gray, 1.3, 10)</pre><pre>  if len(boxes) == 2:<br>    eyes = []<br>    for box in boxes:<br>      x, y, w, h = box<br>      eye = frame[y:y + h, x:x + w]<br>      eye = cv2.resize(eye, image_size)<br>      eye = normalize(eye)<br>      eye = eye[10:-10, 5:-5]<br>      eyes.append(eye)</pre><pre>    return (np.hstack(eyes) * 255).astype(np.uint8)<br>  else:<br>    return None</pre><p>Let’s define the dimensions of our monitor. You’ll have to change these parameters according to the resolution of your own computer screen:</p><pre># Note that there are actually 2560x1440 pixels on my screen<br># I am simply recording one less, so that when we divide by these<br># numbers, we will normalize between 0 and 1. Note that mouse<br># coordinates are reported starting at (0, 0), not (1, 1)<br>width, height = 2559, 1439</pre><p>Now, let’s load in our data (again, assuming you already defined root). We don’t really care whether it was a right or left click, because our goal is just to predict the mouse position:</p><pre>filepaths = os.listdir(root)<br>X, Y = [], []</pre><pre>for filepath in filepaths:<br>  x, y, _ = filepath.split(&#39; &#39;)<br>  x = float(x) / width<br>  y = float(y) / height<br>  X.append(cv2.imread(root + filepath))<br>  Y.append([x, y])</pre><pre>X = np.array(X) / 255.0<br>Y = np.array(Y)<br>print (X.shape, Y.shape)</pre><p>Let’s define our model architecture:</p><pre>model = Sequential()<br>model.add(Conv2D(32, 3, 2, activation = &#39;relu&#39;, input_shape = (12, 44, 3)))<br>model.add(Conv2D(64, 2, 2, activation = &#39;relu&#39;))<br>model.add(Flatten())<br>model.add(Dense(32, activation = &#39;relu&#39;))<br>model.add(Dense(2, activation = &#39;sigmoid&#39;))<br>model.compile(optimizer = &quot;adam&quot;, loss = &quot;mean_squared_error&quot;)<br>model.summary()</pre><p>Here’s our summary:</p><pre>_________________________________________________________________<br>Layer (type)                 Output Shape              Param #<br>=================================================================<br>conv2d (Conv2D)              (None, 5, 21, 32)         896<br>_________________________________________________________________<br>conv2d_1 (Conv2D)            (None, 2, 10, 64)         8256<br>_________________________________________________________________<br>flatten (Flatten)            (None, 1280)              0<br>_________________________________________________________________<br>dense (Dense)                (None, 32)                40992<br>_________________________________________________________________<br>dense_1 (Dense)              (None, 2)                 66<br>=================================================================<br>Total params: 50,210<br>Trainable params: 50,210<br>Non-trainable params: 0<br>_________________________________________________________________</pre><p>Let’s train our model. We’ll add some noise to the image data:</p><pre>epochs = 200<br>for epoch in range(epochs):<br>  model.fit(X, Y, batch_size = 32)</pre><p>Not, let’s use our model to move the mouse with our eyes live. Note that this requires a lot of data to work well. However, as a proof of concept, you’ll notice that with just around 200 images, it does, infact, move the mouse to the general region you are looking at. It’s certainly not controllable until you have much more data though.</p><pre>while True:<br>  eyes = scan()</pre><pre>  if not eyes is None:<br>      eyes = np.expand_dims(eyes / 255.0, axis = 0)<br>      x, y = model.predict(eyes)[0]<br>      pyautogui.moveTo(x * width, y * height)</pre><p>Here’s a proof-of-concept example. Note that I trained with very little data before taking this screen recording. This is a video of my mouse automatically moving to the Terminal application window according to my eyes. As I said, it’s jumpy because there’s very little data. With much more data, it will hopefully be stable enough to control with higher specificity. With just a few hundred images, you’ll only be able to move it to within the general region of your gaze. Also, if throughout your data collection, no images were taken of you looking at a particular region of the screen (say, the edges), the model is unlikely to ever predict within this region. This is one of the many reasons we need more data.</p><p><a href="https://drive.google.com/file/d/1TkU3rRS68U9vk4t7AB--NEQgyYH77Irm/view?usp=sharing">eye_mouse_movement.mp4</a></p><p>If you are testing the code yourself, remember to change the values of width and height to your monitor’s resolution in the code file prediction.py.</p><p>You can view the code from this tutorial here:</p><p><a href="https://github.com/Ryan-Rudes/eye_mouse_movement">Ryan-Rudes/eye_mouse_movement</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f1097e7cf2e9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/controlling-a-mouse-with-your-eyes-f1097e7cf2e9">Controlling a Mouse With Your Eyes</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[An Introductory Reinforcement Learning Project: Learning Tic-Tac-Toe via Self-Play Tabular…]]></title>
            <link>https://medium.com/data-science/an-introductory-reinforcement-learning-project-learning-tic-tac-toe-via-self-play-tabular-b8b845e18fe?source=rss-7b60671f7b73------2</link>
            <guid isPermaLink="false">https://medium.com/p/b8b845e18fe</guid>
            <category><![CDATA[tic-tac-toe]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[reinforcement-learning]]></category>
            <dc:creator><![CDATA[Ryan Rudes]]></dc:creator>
            <pubDate>Tue, 08 Sep 2020 17:30:03 GMT</pubDate>
            <atom:updated>2020-11-21T19:34:36.214Z</atom:updated>
            <content:encoded><![CDATA[<h3>An Introductory Reinforcement Learning Project: Learning Tic-Tac-Toe via Self-Play Tabular Q-learning</h3><p>In this project, I’ll walk through an introductory project on tabular Q-learning. We’ll train a simple RL agent to be able to evaluate tic-tac-toe positions in order to return the best move by playing against itself for many games.</p><p>First, let’s import the required libraries</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a784d1b98d80a7b51fd99be3c06f8865/href">https://medium.com/media/a784d1b98d80a7b51fd99be3c06f8865/href</a></iframe><p>Note that tabular q-learning only works for environments which can be represented by a reasonable number of actions and states. Tic-tac-toe has 9 squares, each of which can be either an X, and O, or empty. Therefore, there are approximately 3⁹ = 19683 states (and 9 actions, of course). Therefore, we have a table with 19683 x 9 = 177147 cells. This is not small, but it is certainly feasible for tabular q-learning. In fact, we could exploit the fact that the game of tic-tac-toe is unchanged by rotations of the board. Therefore, there are actually far fewer “unique states”, if you consider rotations and reflections of a particular board configuration the same. I won’t get into deep Q-learning, because this is intended to be an introductory project.</p><p>First, we initialize our q-table with the aforementioned shape:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/360a2fac0956f451ef3d695fdfa2b59e/href">https://medium.com/media/360a2fac0956f451ef3d695fdfa2b59e/href</a></iframe><p>Now, let’s set some hyperparameters for training:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/504e132db1ac67a0e58921498d080765/href">https://medium.com/media/504e132db1ac67a0e58921498d080765/href</a></iframe><p>Now, we need to set up an exploration strategy. Assuming you understand exploration-exploitation in RL, the <em>exploration strategy</em> is the way we will gradually decrease epsilon (the probability of taking random actions). We need to initially play at least semi-randomly in order to properly explore the environment (the possible tic-tac-toe board configurations). But we cannot forever take random actions, because RL is an iterative process that relies under the assumption that the evaluation of future reward gets better over time. If we simply played random games forever, we would be trying to associate a random list of actions with some final game result that has no actual dependency upon any particular action we took.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/fdd47ee186d096d38291435f9ccfb9f3/href">https://medium.com/media/fdd47ee186d096d38291435f9ccfb9f3/href</a></iframe><p>Now, let’s create a graph of epsilon vs. episodes (number of games simulated) with matplotlib, saving the figure to an image file:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f6773c30ca19130ba7af0a3cd0e7b823/href">https://medium.com/media/f6773c30ca19130ba7af0a3cd0e7b823/href</a></iframe><p>When we start to simulate games, we need to set some restrictions so that the agents can’t make insensible moves. In tic-tac-toe, occupied squares are no longer available, so we need a function to return the legal moves, given a board configuration. We will be representing our board by a 3x3 NumPy array, where unoccupied squares are 0, X’s are 1, and O’s are -1. We can use NumPy’s np.argwhereto retrieve the indices of the 0 elements.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/74631fe9ceee9de5558e89fb617a23db/href">https://medium.com/media/74631fe9ceee9de5558e89fb617a23db/href</a></iframe><p>We also need a helper function to convert between a 3x3 board representation and an integer state. We’re storing the future reward estimations in a q-table, so we need to be able to index any particular board configuration with ease. My algorithm for converting between the board in the format I previously described works by partitioning the total number of possible states into a number of sections corresponding to the number of actions. For each cell in the board:</p><ul><li>If the cell is -1, you don’t change state</li><li>If the cell is 0, you change state by one-third of the window size</li><li>If the cell is 1, you change state by two-thirds of the window size.</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/4061d69b5821b802f6e1d1702becd9d1/href">https://medium.com/media/4061d69b5821b802f6e1d1702becd9d1/href</a></iframe><p>Finally, we need one last helper function to determine when the game has reached a terminal state. This function also needs to return the result of the game if it is indeed over. My implementation checks the rows, columns, and diagonals for a series of either 3 consecutive 1&#39;s or 3 consecutive -1’s by taking the sum of the board array across each axis. This produces 3 sums, one for each axis or column. If -3 is one of these sums, this axis must have all -1’s, indicating that the player corresponding to -1 won and vice versa. The diagonals work just the same, except there are only 2 diagonals, while there are 3 rows and 3 columns. My original implementation is a bit naive, I found a much better one online. It’s much shorter and improves speed slightly.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/953c3a10b08814ac13c09f3bfac10daf/href">https://medium.com/media/953c3a10b08814ac13c09f3bfac10daf/href</a></iframe><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e3bf8a1054d70d3f12ffed024f6f562f/href">https://medium.com/media/e3bf8a1054d70d3f12ffed024f6f562f/href</a></iframe><p>Now, let’s initialize some lists to record training metrics.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/69e65d949fece60edb49eae783db43cf/href">https://medium.com/media/69e65d949fece60edb49eae783db43cf/href</a></iframe><p>past_results will store the results of each simulated game, with 0 representing a tie, 1 indicating that the player corresponding to the positive integer won, and vice versa with -1.</p><p>win_probs will store a list of percentages, updated after each episode. Each value tells the fraction of games up to the current episode in which either player has won. draw_probs also records percentages, but corresponding to the fraction of games in which a draw occurred.</p><p>After training, if we were to graph win_probs and draw_probs, they should demonstrate the following behavior.</p><ol><li>Early in training, the win probability will be high, while the draw probability will be low. This is because when both opponents are taking random actions in a game like tic-tac-toe, there will more often be wins than draws simply due to the existence of a larger number of win states than draw states.</li><li>Mid-way through training, when the agent begins to play according to its table’s policy, the win and draw probabilities will fluctuate with symmetry across the 50% line. Once the agent starts playing competitively against itself, it will encounter more draws, as both sides are playing according to the same strategic policy. Each time the agent discovers a new offensive strategy, there will be a fluctuation in the graph, for the agent is able to trick its opponent (itself) for a short period of time.</li><li>After fluctuating for a while, draw probabilities should approach 100%. If the agent was truly playing optimally against itself, it would always encounter a draw, for it is attempting to maximize reward according to a table of expected future rewards… the same table being used by the opponent (itself).</li></ol><p>Let’s write the training script. For each episode, we begin at a non-terminal state: an empty 3x3 board filled with 0’s. It each move, with some probability epsilon, the agent takes a random action from the list of available squares. Otherwise, it looks up the row of the q-table corresponding to the current state and selects the action which maximizes the expected future reward. The integer representation of the new board state is computed, and we record the pair (s, a, s’). Once this game ends, we will need to correlate the state-action pair we just observed with the final game results (which are yet-to-be-determined). Once the game ends, we refer back to each recorded state-action pair, and update the corresponding cell of the q-table according to the following:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/428/1*C0x4jq6QtELoCixMQhQDOQ.png" /><figcaption>Q-learning update rule</figcaption></figure><p>In the above update formula, s is the integer representation of the state, a is the integer representation of the action the agent took at state s , alpha is the learning rate, R(s, a) is the reward (in our case, the end result of the corresponding game in which this pair (s, a) was observed), Q is the q-table, and the statement involving max represents the maximum expected reward for the resulting state. Say the board configuration was:</p><pre>[[0, 0, 0],<br> [0, 0, 0],<br> [0, 0, 1]]</pre><p>and we took the action 3, corresponding to the cell at coordinate (1, 0), the resulting state would be:</p><pre>[[0  0, 0],<br> [-1, 0, 0],<br> [0, 0, 1]]</pre><p>This part of the update formula is referring to the maximum expected reward for any of the actions we could take from here, according to the policy defined by our current q-table. Therefore, s&#39; is the second state I just described, and a&#39; is all of the actions we could theoretically take from this state (0–8), although in reality, some are illegal (but this is irrelevant).</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b401b4ef92eeb053fcd8ebbb0c3b1146/href">https://medium.com/media/b401b4ef92eeb053fcd8ebbb0c3b1146/href</a></iframe><p>At the end of every 1000 episodes, I just save the list of training metrics, and a plot of these metrics. At the end, I save the q-table and the lists storing these training metrics.</p><h3>Results</h3><p>I trained mine with Google Colab’s online GPU, but you can train yours locally if you’d like; you don’t necessarily have to train all the way to convergence to see great results.</p><p>Just as I previously mentioned, the relationship between games terminating in a win/loss and those terminating in a draw should work as follows:</p><ul><li>Earlier in training, an unskilled, randomly-playing agent will frequently encounter win-loss scenarios.</li><li>Each time the agent discovers a new strategy, there will be fluctuations.</li><li>Towards the end of training, near convergence, the agent will almost always encounter a draw, as it is playing optimally against itself.</li></ul><p>Therefore, the larger fluctuations in the graph indicate moments when the agent learned to evaluate a particular board configuration very well, and in doing so this allowed it to prevent draws.</p><p>We can see this is clearly demonstrated in the resulting graph:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0v8SaynBIwfEpmbMHefJsQ.png" /><figcaption>Win/Loss-Draw Ratio Throughout Training</figcaption></figure><p>Throughout the middle of training, it frequently appears as if the q-table will converge, only to quickly change entirely. These are the aforementioned moments when a significant strategy was exploited for the first time.</p><p>Also, as you can see, the fluctuations occur more rarely as you progress throughout training. This is due to the fact that there are less yet-to-be-discovered tactics as your progress. Theoretically, if the agent converged, there would never be any more great fluctuations like this. Draws would occur 100% of the time and after the rapid rise in the draw percentage, it would not fall back down again.</p><p>I decided it would be a good idea to visualize the change in the q-values over time, so I retrained it while recording the sum of the absolute values of the Q table at each episode. Whether a particular q-value is positive or negative recording the sum of all absolute q-values shows us when convergence is occurring (the gradient of the q-values over time decreases as we reach convergence).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JBYJaMxu_iCo-PruSxxJNQ.png" /><figcaption>Win/Loss-Draw Ratio Throughout Training + Sum of Absolute Value of Q-table throughout Training</figcaption></figure><p>You can visit the full code on Google Colab here:</p><p><a href="https://colab.research.google.com/drive/1w3RYXZ_tg80qNDQZf1I2KyZcigwz8Not?usp=sharing">Google Colaboratory</a></p><p>Or on GitHub here:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/cd2ea755c58aa567b82e3c799593dca4/href">https://medium.com/media/cd2ea755c58aa567b82e3c799593dca4/href</a></iframe><p>Experimenting with the exploration strategy will influence training. You can change parameters relating to epsilon, as well as how it is decayed in order to get different results.</p><p>One final thing to note is that Tic-Tac-Toe can be approached much more easily with simpler value iteration methods because the transition matrix is a given, as well as the reward matrix. This sort of epsilon-greedy optimization is really unnecessary for an environment like Tic-Tac-Toe.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b8b845e18fe" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/an-introductory-reinforcement-learning-project-learning-tic-tac-toe-via-self-play-tabular-b8b845e18fe">An Introductory Reinforcement Learning Project: Learning Tic-Tac-Toe via Self-Play Tabular…</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Style Transfer for Line Drawings]]></title>
            <link>https://medium.com/data-science/style-transfer-for-line-drawings-3c994492b609?source=rss-7b60671f7b73------2</link>
            <guid isPermaLink="false">https://medium.com/p/3c994492b609</guid>
            <category><![CDATA[style-transfer]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[Ryan Rudes]]></dc:creator>
            <pubDate>Sun, 06 Sep 2020 22:58:51 GMT</pubDate>
            <atom:updated>2020-09-09T20:06:34.385Z</atom:updated>
            <content:encoded><![CDATA[<h4>Generating Images From Line Drawings With ML</h4><p>Here, I’ll walk through a machine learning project I recently did in a tutorial-like manner. It is an approach to generating full images in an artistic style from line drawings.</p><h3>Dataset</h3><p>I trained on 10% of the <a href="http://www.image-net.org/">Imagenet</a> dataset. This is a dataset commonly used for benchmarks in computer vision tasks. The Imagenet dataset is not openly available; it is restricted to those undergoing research which requires use of it to compute performance benchmarks for comparing with other approaches. Therefore, it is typically required that you submit a request form. But if you are just using it casually, it is available <a href="https://academictorrents.com/collection/imagenet-2012">here</a>. I just wouldn’t use this for beyond anything beyond personal projects. Note that the dataset is very large, which is why I only used 1/10th of it to train my model. It consists of 1000 classes, so I used 100 of these image classes for training.</p><p>I used Imagenet for a different personal project a few weeks ago, so I already had a large collection of files in Google Drive. Unfortunately, however, it took approximately 20 hours to upload these 140,000 images or so to Google Drive. It is necessary to train the model on Google Colab’s online GPU, but this requires you to upload the images to Google Drive, as you aren’t hosting your coding environment locally.</p><h3>Data Input Pipeline</h3><p>I have a Colab Pro account, but even with the additional RAM, I certainly can’t handle 140,000 line drawing, each of 256x256 pixels in size, along with their 256x256 pixel colored counterparts. Hence, I have to load in the data on-the-go using a TensorFlow data input pipeline.</p><p>Before we start to set up the pipeline, let’s import the required libraries (these are all of the import statements in my code):</p><pre>import matplotlib.pyplot as plt<br>import numpy as np<br>import cv2<br>from tqdm.notebook import tqdm<br>import glob<br>import random<br>import threading, queue</pre><pre>from tensorflow.keras.models import *<br>from tensorflow.keras.layers import *<br>from tensorflow.keras.optimizers import *<br>from tensorflow.keras.regularizers import *<br>from tensorflow.keras.utils import to_categorical<br>import tensorflow as tf</pre><p>Now, let’s load the filepaths which refer to each image in our subset of Imagenet, assuming you have uploaded them to Drive under the appropriate directory structure and connection your Google Colab instance to your Google Drive.</p><pre>filepaths = glob.glob(&quot;drive/My Drive/1-100/**/*.JPEG&quot;)</pre><pre># Shuffle the filepaths<br>random.shuffle(filepaths)</pre><p>If you don’t want to use the glob module, you can use functions from theos library, which are often more efficient.</p><p>Here’s a few helper functions I need:</p><ul><li>Normalizing data</li><li><em>Posterizing</em> image data</li></ul><pre>def normalize(x):<br>  return (x - x.min()) / (x.max() - x.min())</pre><h4>Posterization</h4><p>The aforementioned process of posterization takes an image as input and transforms smooth gradients into more clearly-separated color sections by rounding color values to some nearest value. Here’s an example:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*iE5DY2GY2iEp_CptkhMJ5Q.png" /><figcaption>Posterization</figcaption></figure><p>As you can see, the resulting image has less smooth gradients, which are replaced with separated color sections. The reason I am implementing this is because I can limit the output images to a set of colors, allowing me to format the learning problem as a classification problem across each pixel in an image. For each available color, I assign a label. The model outputs an image of shape (height, width, num_colors) activated by a softmax function over the last channel, num_colors. Given a variable num_values, I allow all combinations of RGB where the color values are limited to np.arange(0, 255, 255 / num_values). This means that num_colors = num_values ** 3. Here’s an example:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6vGtr9cRNhykPm6q2Fsqvw.png" /><figcaption>Posterization</figcaption></figure><p>For an example of how I implemented this, here’s a demonstration:</p><pre>def get_nearest_color(color, colors):<br>  &quot;&quot;&quot;<br>  Args:<br>   - color: A vector of size 3 representing an RGB color<br>   - colors: NumPy array of shape (num_colors, 3)<br>  Returns:<br>   - The index of the color in the provided set of colors that is<br>     closest in value to the provided color<br>  &quot;&quot;&quot;</pre><pre>  return np.argmin([np.linalg.norm(color - c) for c in colors])</pre><pre>def posterize_with_limited_colors(image, colors):<br>  &quot;&quot;&quot;<br>  Args:<br>   - colors: NumPy array of shape (num_colors, 3)<br>  Returns:<br>   - Posterized image of shape (height, width, 1), where each value <br>     is an integer label associated with a particular index of the<br>     provided colors array<br>  &quot;&quot;&quot;</pre><pre>  image = normalize(image)<br>  posterized = np.array([[get_nearest_color(x, colors) for x in y] for y in image])<br>  return posterized</pre><h4>Edge Extraction</h4><p>In order to create the input data from our colored images, we need a method of extracting edges from an image which are akin to a trace or line drawing.</p><p>We’ll be using the <a href="https://en.wikipedia.org/wiki/Canny_edge_detector">Canny edge detection algorithm</a>. Let’s write our helper function, which inputs the path to an image and output the associated example/(X, Y) training pair, comprised of a posterization of the colored input, alongside the black and white edge extraction:</p><pre>def preprocess(path):<br>  color = cv2.imread(path)<br>  color = cv2.resize(color, input_image_size)</pre><pre>  # Assuming your pipelines generator function ignores None<br>  if color.shape &lt; input_image_size:<br>    return None, None</pre><pre>  color = (normalize(color) * 255).astype(np.uint8)</pre><pre>  gray = cv2.cvtColor(color, cv2.COLOR_RGB2GRAY<br>  # Removes noise while preserving edges<br>  filtered = cv2.bilateralFilter(gray, 3, 60, 120)</pre><pre>  # Automatically determine threshold for edge detection algorithm<br>  # based upon the median color value of the image<br>  m = np.median(filtered)<br>  preservation_factor = 0.33<br>  low = max(0, int(m - 255 * preservation_factor))<br>  high = int(min(255, m + 255 * preservation_factor))<br>  filtered_edges = cv2.Canny(filtered, low, high)<br>  filtered_edges = normalize(filtered_edges)<br>  filtered_edges = np.expand_dims(filtered_edges, axis = -1)</pre><pre>  color = cv2.resize(color, output_image_size)<br>  color /= 255.<br>  color = posterize_with_limited_colors(color, colors)</pre><pre>  return filtered_edges, color</pre><p>The automatic Canny edge detection is just my modification to the small function used in <a href="https://www.pyimagesearch.com/2015/04/06/zero-parameter-automatic-canny-edge-detection-with-python-and-opencv/">this</a> article.</p><h4>The Pipeline</h4><p>As I said, I’m loading in data on-the-spot using an input pipeline. Therefore, I need to define a generator object to load in this data when needed. My generator function is simple because we basically just defined it. All it adds is filtering out the None outputs of the preprocess function (images of lower resolution than input_image_size and filtering out any results containing nan or inf values.</p><pre>def generate_data(paths):<br>  for path in paths:<br>    edges, color = preprocess(path.decode())<br>    if not edges is None:<br>      if not np.any(np.isnan(edges)) or np.any(np.isnan(color)):<br>        if not np.any(np.isinf(edges)) or np.any(np.isinf(color))):<br>          # Yield the clean data<br>          yield edges, color</pre><p>I use (128, 128) for both input_image_size and output_image_size. A 128x128 pixel image isn’t <strong>that </strong>low-resolution, so there’s no significant disadvantage for our purposes. Also, Imagenet images are typically much higher resolution, so we can go higher if desired.</p><p>Now let’s build the pipeline. I’m using multithreading for improved speeds. TensorFlow’s.interleave()allows us to do this:</p><pre>thread_names = np.arange(0, 8).astype(str)<br>dataset = tf.data.Dataset.from_tensor_slices(thread_names)</pre><pre>dataset = dataset.interleave(lambda x:<br>  tf.data.Dataset.from_generator(<br>    generate_data,<br>    output_types = (tf.float32, tf.float32),<br>    output_shapes = ((*input_image_size, 1),<br>                     (*output_image_size, 1)),<br>    args = (train_paths,)),<br>    cycle_length = 8,<br>    block_length = 8,<br>    num_parallel_calls = 8)</pre><pre>dataset = dataset.batch(batch_size).repeat()</pre><h4>Testing The Pipeline</h4><p>Let’s load in a training example through our pipeline:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/572/1*-M_BHRyGO6RLS_StW6nMdg.png" /><figcaption>One training example with input line drawing/edges (right) and output colorization (left)</figcaption></figure><p>It’s exactly as desired. Note that the image depicted on the left is not exactly what was outputted by the pipeline. Recall that the pipeline is returning the index referring to the color of each pixel. I simply referred to each associated color to create the visualization. Here’s an example of one that came out much simpler.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/572/1*6bVWk6OcwKShF4nvqRZGig.png" /><figcaption>Simpler training example</figcaption></figure><p>You’ll see that on the left we have the output, posterized color image, which partially resembles a painting. On the right, you see the input edge extraction, which resembles a sketch.</p><p>Of course not all training examples will have as good of an edge extraction than others. When the colors are more difficult to separate, the resulting outline might be a little noisy and/or scattered. However, this was the most accurate method for extracting edges I could think of.</p><h3>Model Architecture</h3><p>Let’s move on to the model architecture.</p><p>I begin at input_image_size = (128, 128), thus making the input of shape (128, 128, 1) after expanding the last axis. I decrease the layer input shape by a power of 2 until it equals 1. Then, I apply two more convolutional layers with stride = 1, because we can’t decrease the shape of the first two axes any further. Then, I perform the reverse with transposed layers. Each convolutional layer has padding = &#39;valid&#39; and there is a batch normalization layer between each convolutional layer. All convolution layers have ReLU activation, except the last, which of course has softmax activation over the final one-hot-encoded color-label channel.</p><pre>_________________________________________________________________ Layer (type)                 Output Shape              Param #    ================================================================= input_35 (InputLayer)        [(None, 128, 128, 1)]     0          _________________________________________________________________ conv2d_464 (Conv2D)          (None, 64, 64, 3)         30         _________________________________________________________________ batch_normalization_388 (Bat (None, 64, 64, 3)         12         _________________________________________________________________ conv2d_465 (Conv2D)          (None, 32, 32, 9)         252        _________________________________________________________________ batch_normalization_389 (Bat (None, 32, 32, 9)         36         _________________________________________________________________ conv2d_466 (Conv2D)          (None, 16, 16, 27)        2214       _________________________________________________________________ batch_normalization_390 (Bat (None, 16, 16, 27)        108        _________________________________________________________________ conv2d_467 (Conv2D)          (None, 8, 8, 81)          19764      _________________________________________________________________ batch_normalization_391 (Bat (None, 8, 8, 81)          324        _________________________________________________________________ conv2d_468 (Conv2D)          (None, 4, 4, 243)         177390     _________________________________________________________________ batch_normalization_392 (Bat (None, 4, 4, 243)         972        _________________________________________________________________ conv2d_469 (Conv2D)          (None, 2, 2, 729)         1595052    _________________________________________________________________ batch_normalization_393 (Bat (None, 2, 2, 729)         2916       _________________________________________________________________ conv2d_470 (Conv2D)          (None, 1, 1, 2187)        14351094   _________________________________________________________________ batch_normalization_394 (Bat (None, 1, 1, 2187)        8748       _________________________________________________________________ conv2d_471 (Conv2D)          (None, 1, 1, 2187)        43048908   _________________________________________________________________ batch_normalization_395 (Bat (None, 1, 1, 2187)        8748       _________________________________________________________________ conv2d_472 (Conv2D)          (None, 1, 1, 2187)        43048908   _________________________________________________________________ batch_normalization_396 (Bat (None, 1, 1, 2187)        8748       _________________________________________________________________ conv2d_transpose_229 (Conv2D (None, 1, 1, 2187)        43048908   _________________________________________________________________ batch_normalization_397 (Bat (None, 1, 1, 2187)        8748       _________________________________________________________________ conv2d_transpose_230 (Conv2D (None, 1, 1, 2187)        43048908   _________________________________________________________________ batch_normalization_398 (Bat (None, 1, 1, 2187)        8748       _________________________________________________________________ conv2d_transpose_231 (Conv2D (None, 2, 2, 2187)        43048908   _________________________________________________________________ batch_normalization_399 (Bat (None, 2, 2, 2187)        8748       _________________________________________________________________ conv2d_transpose_232 (Conv2D (None, 4, 4, 2187)        43048908   _________________________________________________________________ batch_normalization_400 (Bat (None, 4, 4, 2187)        8748       _________________________________________________________________ conv2d_transpose_233 (Conv2D (None, 8, 8, 729)         14349636   _________________________________________________________________ batch_normalization_401 (Bat (None, 8, 8, 729)         2916       _________________________________________________________________ conv2d_transpose_234 (Conv2D (None, 16, 16, 243)       1594566    _________________________________________________________________ batch_normalization_402 (Bat (None, 16, 16, 243)       972        _________________________________________________________________ conv2d_transpose_235 (Conv2D (None, 32, 32, 81)        177228     _________________________________________________________________ batch_normalization_403 (Bat (None, 32, 32, 81)        324        _________________________________________________________________ conv2d_transpose_236 (Conv2D (None, 64, 64, 27)        19710      _________________________________________________________________ up_sampling2d_1 (UpSampling2 (None, 128, 128, 27)      0          _________________________________________________________________ batch_normalization_404 (Bat (None, 128, 128, 27)      108        ================================================================= Total params: 290,650,308 Trainable params: 290,615,346 Non-trainable params: 34,962 _________________________________________________________________</pre><h3>Training</h3><p>Let’s create some lists to store out metrics throughout training.</p><pre>train_losses, train_accs = [], []</pre><p>Also, a variable for the number of training epochs</p><pre>epochs = 100</pre><p>And here’s our training script</p><pre>for epoch in range(epochs):<br>  random.shuffle(filepaths)<br>  history = model.fit(dataset,<br>                      steps_per_epoch = steps_per_epoch,<br>                      use_multiprocessing = True,<br>                      workers = 8,<br>                      max_queue_size = 10)</pre><pre>  train_loss = np.mean(history.history[&quot;loss&quot;])<br>  train_acc = np.mean(history.history[&quot;accuracy&quot;])</pre><pre>  train_losses = train_losses + history.history[&quot;loss&quot;]<br>  train_accs = train_accs + history.history[&quot;accuracy&quot;]</pre><pre>  print (&quot;Epoch: {}/{}, Train Loss: {:.6f}, Train Accuracy: {:.6f}, Val Loss: {:.6f}, Val Accuracy: {:.6f}&quot;.format(epoch + 1, epochs, train_loss, train_acc, val_loss, val_acc))</pre><pre>  if epoch &gt; 0:<br>    fig = plt.figure(figsize = (10, 5))<br>    plt.subplot(1, 2, 1)<br>    plt.plot(train_losses)<br>    plt.xlim(0, len(train_losses) - 1)<br>    plt.xlabel(&quot;Epoch&quot;)<br>    plt.ylabel(&quot;Loss&quot;)<br>    plt.title(&quot;Loss&quot;)<br>    plt.subplot(1, 2, 2)<br>    plt.plot(train_accs)<br>    plt.xlim(0, len(train_accs) - 1)<br>    plt.ylim(0, 1)<br>    plt.xlabel(&quot;Epoch&quot;)<br>    plt.ylabel(&quot;Accuracy&quot;)<br>    plt.title(&quot;Accuracy&quot;)<br>    plt.show()</pre><pre>  model.save(&quot;model_{}.h5&quot;.format(epoch))<br>  np.save(&quot;train_losses.npy&quot;, train_losses)<br>  np.save(&quot;train_accs.npy&quot;, train_accs)</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3c994492b609" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/style-transfer-for-line-drawings-3c994492b609">Style Transfer for Line Drawings</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Wav2Lip: A Lip Sync Expert Is All You Need for Speech to Lip Generation In The Wild]]></title>
            <link>https://medium.com/data-science/wav2lip-a-lip-sync-expert-is-all-you-need-for-speech-to-lip-generation-in-the-wild-b1cb48787190?source=rss-7b60671f7b73------2</link>
            <guid isPermaLink="false">https://medium.com/p/b1cb48787190</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[wav2lip]]></category>
            <dc:creator><![CDATA[Ryan Rudes]]></dc:creator>
            <pubDate>Fri, 04 Sep 2020 15:11:33 GMT</pubDate>
            <atom:updated>2020-09-09T03:27:02.827Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bWkXdSzk-wLOAlI8ouH6FQ.png" /><figcaption>Wav2Lip Model Architecture (<a href="https://arxiv.org/pdf/2008.10010v1.pdf">https://arxiv.org/pdf/2008.10010v1.pdf</a>)</figcaption></figure><p>This paper proposes Wav2Lip, an adaptation of the SyncNet model, which outperforms all prior speaker-independent approaches towards the task of video-audio lip-syncing.</p><p>The authors note that, while prior approaches typically fail to generalize when presented with video of speakers not present in the training set, Wav2Lip is capable of producing accurate lip movements with a variety of speakers.</p><p>They continue to summarize the primary intentions of the paper:</p><ol><li>Identifying the cause of prior approaches failing to generalize to a variety of speakers.</li><li>Resolve said issues by incorporating a powerful lip-sync discriminator.</li><li>Propose new benchmarks for evaluating the performance of approaches towards the task of lip-syncing.</li></ol><h3>Introduction</h3><p>The authors first point regards the recent boom in the consumption of video and audio content. Alongside this, there is a growing need for audio-video translation across a variety of languages in order to promote accessibility to a greater portion of the public. Thus, there is a significant motivation for applying machine learning to such a task as automated lip-syncing of unconstrained video-audio content.</p><p>Unfortunately, however, earlier approaches commonly failed to generalize to a variety of speakers identities, only performing well when evaluated on the small subset of potential speakers which comprised their training set.</p><p>Such approaches will fail to meet the rigorous requirements of the aforementioned practical application, where a suitable mode would have to be capable of accurately syncing a variety of speakers.</p><p>Due to the demanding requirements of using such an approach in practice, one requires a model which can generalize to a variety of speaker identities. As a result, speaker-independent approaches have arose. These models are trained on thousands of speaker identities. However, even these approaches employed in prior publications fail to meet the expectations of the authors of this work. They acknowledge that prior speaker independent models, while being able to generate accurate lip-syncing on individual static images, are inapplicable to dynamic content. For applications such as the translation of television series and films, it is required that an approach is more generalizable to the varying lip shapes of the speakers in different unconstrained videos.</p><p>The authors cite that a video segment approximately 0.05–0.1 seconds out-of-sync is detectable by a human, thus implying a broad challenge with a fine margin for error.</p><p>The section concludes with a brief summary of the authors contributions:</p><ol><li>They propose <em>Wav2Lip</em>, which significantly outperforms prior approaches.</li><li>They introduce a new set of benchmarks/metrics for evaluating the performance of models in this task.</li><li>They release their own dataset to evaluate the performance of their approach when presented with unseen video-audio content sampled from the wild.</li><li>Wav2Lip is the first speaker-independent approach which frequently matches the accuracy of real synced videos; according to human evaluation, their approach is preferred to existing methods approximately 90% of the time.</li><li>They push the FID score on generating synchronous video frames for dubbed videos from 12.87 (LipGAN) to 11.84 (Wav2Lip + GAN), improving the average user-preference from 2.35% (LipGAN) to 60.2% (Wav2Lip + GAN).</li></ol><h3>Review of Existing Literature</h3><p>(I’ll limit this section to the author’s reasons for mentioning these papers, and leave out the in-depth information regarding specifically the approaches of these works)</p><p>The authors acknowledge several discrepancies between prior approaches and the requirements for an approach to work fully in the real world:</p><ol><li>Requiring a large amount of training data for some methods.</li><li>Limitations in terms of the extent of vocabulary learned by the model.</li><li>Training on datasets with a limited set of vocabulary impedes upon the ability of prior approaches to learn the wide variety of phoneme-viseme mappings.</li></ol><p>They continue to argue why prior approaches commonly fail to generate accurate lip-syncing when presented with unseen video content from the wild:</p><ol><li><em>Pixel-level Reconstruction loss is a Weak Judge of Lip-sync</em>: Loss functions incorporated in prior works inadequately penalize inaccurate lip-sync generation.</li><li><em>A Weak Lip-sync Discriminator</em>: The discriminator in the LipGAN model architecture only has a 56% accuracy at detecting off-sync video-audio content, while the discriminator of Wav2Lip is 91% accurate at distinguishing in-sync content from off-sync content on the same test set.</li></ol><h4>A Lip-sync Expert Is All You Need</h4><p>Finally, the authors propose their approach, taking into considering both of the above issues in prior works.</p><ul><li>Use a pre-trained lip-sync discriminator that is already accurate in detecting out-of-sync video-audio content in raw, unconstrained samples.</li><li>Adapt the previously existing SyncNet model for this task. (I won’t go into depth about this, rather, I’ll only emphasize on the Wav2Lip architecture).</li></ul><h3>Overview of the Wav2Lip Model Architecture</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bWkXdSzk-wLOAlI8ouH6FQ.png" /><figcaption>Wav2Lip Model Architecture (<a href="https://arxiv.org/pdf/2008.10010v1.pdf">https://arxiv.org/pdf/2008.10010v1.pdf</a>)</figcaption></figure><h4>Terminology</h4><p>The authors use the following terms to refer to the various sections of their network, which I will continue to use following this section:</p><ul><li><em>Random reference segment</em>: A random sample of a segment of consecutive frames used to identify a particular speaker, providing the network context of the identity specific to the aforementioned speaker.</li><li><em>Identity encoder</em>: Encodes the concatenation of the ground truth frames and a random reference segment, providing visual context for the network to adapt appropriately to any particular speaker.</li><li><em>Speech encoder</em>: Encodes the audio data (self-explanatory).</li><li><em>Face decoder</em>: Decoded the concatenated feature vectors into a series of reconstructed frames.</li></ul><h4>Methodology</h4><p>At a high level, Wav2Lip inputs a Mel-spectrogram representation of a particular audio segment alongside a concatenation of the corresponding ground truth frames (with the bottom half masked) and a random reference segment whose speaker confirms to that of the ground truth segment. It reduces this input via convolutional layers to form a feature vector for both the audio and frames input. It then concatenates these feature representations, projecting the resulting matrix onto a segment of reconstructed frames through a series of transposed convolutional layers. There are residual skip connections between layers of the identity encoder and face decoder.</p><p>Wav2Lip attempts to fully reconstruct the ground truth frames from their masked copies. We compute L1 reconstruction loss between the reconstructed frames and the ground truth frames. Then, the reconstructed frames are fed through a pretrained “expert” lip-sync detector, while both the reconstructed frames and ground truth frames are fed through the Visual Quality Discriminator. The Visual Quality Discriminator attempts to distinguish between reconstructed frames and ground truth frames to promote the visual generation quality of the frame generator.</p><h3>Loss Functions</h3><h4>The Generator</h4><p>The generator aims to minimize the L1 loss between the reconstructed frames $L_g$ and the ground truth frames $L_G$:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/223/1*nhEQc-HmicENfE-cdCDoXQ.png" /></figure><p>where 𝑵 is the generally-accepted notation to denote <em>batch size</em>.</p><h4>The Lip-Sync Discriminator</h4><p>For lip-syncing, they implement cosine similarity with binary cross-entropy loss, thus computing the probability that a given two frames. More specifically, loss is computed between the ReLU-activated video and speech embeddings 𝑣 and 𝑠. This results in a list of probabilities, one for each sample, indicating the probability that the corresponding sample is in sync.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/171/1*1jjqMdOLyovT4-_EC1l6tw.png" /></figure><p>where the ReLU activation applied may be described as:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/212/1*rH5TA8GrdwbATvS2DuPYXw.png" /></figure><p>The full expert discriminator loss is computed by taking the cross-entropy of the distribution $P_{sync}$ as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/222/1*sn7nCeegMjt58x6TTzB7Yg.png" /></figure><h4>The Visual-Quality Discriminator</h4><p>The Visual-Quality Discriminator is trained to maximize the following loss:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/251/1*ndDxFZOw2lCHV0c1sX0_Kg.png" /></figure><p>where the generator loss $L_{gen}$ is formulated as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/220/1*S59sjbEh_4vxIe4zXgkKGg.png" /></figure><p>Accordingly, the generator attempts to minimize the weighted sum of the reconstruction loss, the synchronization loss, and the adversarial loss (recall that we are dealing with two discriminators):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/413/1*tC1AYqZNp6E7qWFqzDk3wg.png" /></figure><p>where $s_w$ is the a weighting value which indicates the penalty attributed to synchronization, and $s_g$ is the adversarial loss.</p><p>These two disjoint discriminators allow the network to achieve superior synchronization-accuracy and visual generation quality.</p><h3>Conclusion and Further Reading</h3><p>That’s it for <em>”A Lip Sync Expert Is All You Need for Speech to Lip Generation In The Wild”</em>. If you’d like to read more in-depth, or about the few things I didn’t cover here:</p><ul><li>The proposed metric/evaluation system</li><li>Benchmark comparisons between Wav2Lip and prior models</li><li>Detailed training procedure and hyperparameters used by the authors</li><li>Real-world evaluation of Wav2Lip</li></ul><p>you can investigate further by reading the paper: <a href="https://arxiv.org/pdf/2008.10010v1.pdf">https://arxiv.org/pdf/2008.10010v1.pdf</a></p><p>Model architecture image credit to the authors of <em>“A Lip Sync Expert Is All You Need for Speech to Lip Generation In The Wild”.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b1cb48787190" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/wav2lip-a-lip-sync-expert-is-all-you-need-for-speech-to-lip-generation-in-the-wild-b1cb48787190">Wav2Lip: A Lip Sync Expert Is All You Need for Speech to Lip Generation In The Wild</a> was originally published in <a href="https://medium.com/data-science">TDS Archive</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>