<?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 Thomas Tracey on Medium]]></title>
        <description><![CDATA[Stories by Thomas Tracey on Medium]]></description>
        <link>https://medium.com/@thomastracey?source=rss-49cc733eeb2e------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*wcQS4rnFNN6XKOCcXTr5lg.png</url>
            <title>Stories by Thomas Tracey on Medium</title>
            <link>https://medium.com/@thomastracey?source=rss-49cc733eeb2e------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 19:05:35 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@thomastracey/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[Recognizing Traffic Signs with CNNs]]></title>
            <link>https://medium.com/@thomastracey/recognizing-traffic-signs-with-cnns-23a4ac66f7a7?source=rss-49cc733eeb2e------2</link>
            <guid isPermaLink="false">https://medium.com/p/23a4ac66f7a7</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[image-classification]]></category>
            <category><![CDATA[convolutional-neural-net]]></category>
            <category><![CDATA[self-driving-cars]]></category>
            <category><![CDATA[udacity]]></category>
            <dc:creator><![CDATA[Thomas Tracey]]></dc:creator>
            <pubDate>Sun, 24 Feb 2019 02:59:51 GMT</pubDate>
            <atom:updated>2019-02-24T21:18:46.828Z</atom:updated>
            <content:encoded><![CDATA[<h4>Build a convolutional neural network that classifies traffic signs.</h4><p><em>July 20, 2017 by </em><a href="https://ttracey.com/"><em>Thomas Tracey</em></a><em>, originally </em><a href="https://github.com/tommytracey/udacity/blob/master/self-driving-nano/projects/2-traffic-signs/README.md"><em>posted on Github</em></a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pAE2_D0jh0zgW1Ipf9UcGw.png" /></figure><p>This post explores my work on the <a href="https://github.com/tommytracey/udacity/tree/master/self-driving-nano/projects/2-traffic-signs">second project</a> of the <a href="https://www.udacity.com/course/self-driving-car-engineer-nanodegree--nd013">Udacity Self-Driving Car Nanodegree</a> program. <strong>My goal is to help other students and professionals who are in the early phases of building their intuition in machine learning (ML) and artificial intelligence (AI).</strong></p><p>With that said, please keep in mind that I am a product manager by trade (not an engineer or data scientist). So, what follows is meant to be a semi-technical yet approachable explanation of the ML concepts and algorithms in this project. If anything covered below is inaccurate or if you have <em>constructive feedback</em>, I’d love to hear from you.</p><p>My Github repo for this project can be found <a href="https://github.com/tommytracey/udacity/tree/master/self-driving-nano/projects/2-traffic-signs">here</a>. The original Udacity source repo for this project is located <a href="https://github.com/udacity/CarND-Traffic-Sign-Classifier-Project">here</a>.</p><h3>Project Goal</h3><p>The goal of this project is to build a neural network that recognizes traffic signs in Germany.</p><p>Udacity’s benchmark for the project is to achieve at least 93% accuracy (on the validation set). However, my personal goal was to surpass the human level performance benchmark of 98.8% accuracy identified in <a href="https://arxiv.org/pdf/1511.02992.pdf">this paper</a> by Mrinal Haloi from the Indian Institute of Technology.</p><h3>Approach</h3><p>The basic steps of the project are as follows:</p><ol><li>Load the data set provided by Udacity</li><li>Explore, summarize and visualize the data set</li><li>Design, train and test a model architecture</li><li>Use the model to make predictions on new images</li><li>Analyze the softmax probabilities of the new images</li><li>Summarize the results with a written report</li></ol><h4>Project Notebook</h4><p>My code and a detailed view of the outputs for each step are outlined here in <a href="https://github.com/tommytracey/udacity/blob/master/self-driving-nano/projects/2-traffic-signs/Traffic_Sign_Classifier_final_v5.ipynb">this Jupyter notebook</a>.</p><h3>Data Summary &amp; Exploration</h3><p>Throughout this section, I use the Numpy, Pandas, and Matplotlib libraries to explore and visualize the traffic signs data set.</p><h4>Data Size &amp; Shape</h4><ul><li>Size of training set: 34,799 (67%)</li><li>Size of the validation set: 4,410 (9%)</li><li>Size of test set: 12,630 (24%)</li><li>Shape of a traffic sign image: (32, 32, 3)</li><li>Number of unique classes/labels: 43</li></ul><h4>Data Visualization</h4><p>Before designing the neural network, I felt it was important to visualize the data in various ways to gain some intuition for what the model will “see.” This not only informs the model structure and parameters, but it also helps me determine what types of preprocessing operations should be applied to the data (if any).</p><p>There are a few fundamental ways I used visualizations to inform my decisions:</p><ol><li><strong>Inspect a sample of images. </strong>Do the images correspond with the expected number of color channels? — i.e., if channels=3 then the images should appear in color/RGB not grayscale. How clear are the images? Is there anything that makes the signs hard to recognize (e.g. bad weather, darkness, glare, occlusions)?</li><li><strong>Review a sample of the labels. </strong>Do the labels make sense? Do they accurately correspond with images in the data set?</li><li><strong>Create a histogram showing the distribution of classes/labels</strong>. How balanced is the dataset? Are there certain classes that dominate? Are there others that are under-represented?</li></ol><h4>Image &amp; Label Sample</h4><p>Below is a sample of the original images before they undergo any preprocessing. Overall, the image quality is good and the labels make intuitive sense. However, immediately you notice a few things we’ll want to adjust during preprocessing:</p><ul><li>Many of the signs are hard to recognize because the <strong>images are dark and have low contrast</strong>.</li><li>There is <strong>little variation in the sign shape and viewing angle</strong>. Most of the pictures are taken with a straight on view of the sign, which is good for the core data set. However, in real life, signs are viewed from different angles.</li><li>The <strong>signs are void of any deformations or occlusions</strong>. Again, this is good because we need a clean set of training samples, but in real life, signs are sometimes damaged, vandalized, or only partially visible. Essentially, we want the model to recognize signs even when the shape is distorted, much like humans can. So, augmenting the training set with a variety of distortions is important.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*dk1Q3A8zh8qN4Esm.jpg" /></figure><h4>Class/Label Distribution</h4><p>As you can see below, the distribution is not uniform. The largest classes have 10x the number of traffic sign images than the smallest classes. This is expected given that in real-life there are certain signs which appear more frequently than others. However, when training the model, I wanted a more uniform distribution so that each class has the same number of training examples and the model, therefore, has an equal number of opportunities to learn each sign.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/835/0*HjQMlJvjMouzwt4e.png" /></figure><h3>Data Preprocessing</h3><p>Given the issues identified above, I decided to explore the following preprocessing operations (in addition to data normalization):</p><h4><strong>Contrast enhancement:</strong></h4><p>I used this Scikit <a href="http://scikit-image.org/docs/dev/api/skimage.exposure.html#skimage.exposure.equalize_adapthist">histogram equalization function</a>, which not only normalizes the images but also enhances local contrast details in regions that are darker or lighter than most of the image (<a href="https://github.com/tommytracey/udacity/blob/master/self-driving-nano/projects/2-traffic-signs/Traffic_Sign_Classifier_final_v5.py#L231">link to source code</a>). You can see from the image sample below this also inherently increases the brightness of the image.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/406/0*L-aCXeSPenyHWKDT.jpg" /></figure><h4>Augmentation</h4><ul><li><strong>Increase the total number of images</strong> so that the model has more training examples to learn from.</li><li><strong>Create an equal distribution of images</strong> — i.e., the same number of images per class, so that the model has a sufficient number of training examples in each class. I initially tested models on sets of 4k images per class and found that models performed better with more images. I ultimately generated a set of 6k images per class for the final model.</li><li><strong>Apply affine transformations</strong>. Used to generate images with various sets of perturbations. Specifically: rotation, shift, shearing, and zoom. But, I decided not to apply horizontal/vertical flipping as this didn’t seem applicable to real-life use cases.</li><li><strong>Apply ZCA whitening</strong> to accentuate edges.</li><li><strong>Apply two types of color transformations:</strong></li><li><strong>(1) Color channel shifts</strong>. This was done to create slight color derivations to help prevent the model from overfitting on specific color shades. This intuitively seemed like a better strategy than grayscaling.</li><li><strong>(2) Grayscaling</strong>. This was performed separately _after_ all of the above transformations. Due to the high darkness and low contrast issues, applying grayscale before the other transformations didn’t make sense. It would only make the contrast issue worse. So, I decided to test the grayscale versions as a separate data set to see if it boosted performance (spoiler alert: it didn’t).</li></ul><p>Below are snippets of code that takes the already normalized images (with contrast-enhanced) and applies the other transformations listed above. It outputs a new training set with 6k images per class, including the set of normalized training images (<a href="https://github.com/tommytracey/udacity/blob/master/self-driving-nano/projects/2-traffic-signs/Traffic_Sign_Classifier_final_v5.py#L285">link to source code</a>).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1016/0*zV8C2siQ1FiRk_Cg.jpg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*XW1PU6vDnjUbG1Bi.jpg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*iaLBT5UGfIn8yWyN.jpg" /></figure><h4>Augmented Image Samples</h4><p>Below is a sample of a traffic sign images after the complete set of normalization, contrast enhancement, and augmentation steps listed above.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*i6eyt2XEvxNifCnf.jpg" /></figure><h4>Grayscaling</h4><p>Here is a sample of images with **grayscaling** then applied. At first glance, it doesn’t appear that grayscaling improves the images in any meaningful way. So, my hypothesis was that the grayscaled versions would perform the same or worse than the augmented images (this turned out to be correct).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ARbJTEN6Xz-3qpJ0.jpg" /></figure><h3>Model Architecture</h3><p>I tested a variety of models (&gt; 25 different combinations). Ultimately, I settled on a relatively small and simple architecture that was easy to train and still delivered great performance. My final model consisted of the following layers:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*LXU6rZlUr2WoeLPp.png" /></figure><h4>Model</h4><p>Below is a snapshot of the model (<a href="https://github.com/tommytracey/udacity/blob/master/self-driving-nano/projects/2-traffic-signs/Traffic_Sign_Classifier_final_v5.py#L633">here</a> is a link to the source code). You can see that I use: (1) a ReLU activation on every layer, (2) maxpooling on alternating convolutional layers with a 5x5 filter, and (3) dropouts on the two fully connected layers with a 0.5 keep probability.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*syzJD6pi7eeA6vkO.jpg" /></figure><h4>Training Loop</h4><p>Below are the training loop, loss, and regularization functions (<a href="https://github.com/tommytracey/udacity/blob/master/self-driving-nano/projects/2-traffic-signs/Traffic_Sign_Classifier_final_v5.py#L713">link to source code</a>). You can see that I use AdamOptimizer to take advantage of its built-in hyperparameter tuning, which varies the learning rate based on moving averages (momentum) to help the model converge faster, without having to manually tune it myself. You’ll notice that I also use L2 regularization to help prevent overfitting.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*MGtm7jYfzNRGn5HA.jpg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/668/0*XWEebL75hhaaOxzk.jpg" /></figure><h4>Hyperparameters</h4><p>Here are the hyperparameters I used (<a href="https://github.com/tommytracey/udacity/blob/master/self-driving-nano/projects/2-traffic-signs/Traffic_Sign_Classifier_final_v5.py#L545">link to source code</a>). My goal was to get the model to converge in less than 50 epochs. Essentially, given time constraints, I didn’t want to spend more than two hours training the model. Everything else is pretty standard. Although, I did decrease my L2 decay rate (i.e. lower penalty on weights) during the tuning process, which yielded a small lift in performance.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/872/0*VtGlTWMp6PilvgOh.jpg" /></figure><h4>Output</h4><p>Here is the output when I construct the graph (<a href="https://github.com/tommytracey/udacity/blob/master/self-driving-nano/projects/2-traffic-signs/Traffic_Sign_Classifier_final_v5.py#L772">link to source code</a>). I use print statements to verify that the model structure matches my expectations. I find this very useful as it’s easy to get confused when you’re tweaking and testing lots of different models. Especially after 3am. 😵</p><h3>Final Model Results:</h3><ul><li>training accuracy: 100%</li><li>validation accuracy: 99.4%</li><li>test accuracy: 98.2%</li></ul><h3>Model Iteration &amp; Tuning</h3><p>Here I’ll try to summarize the approach I took to find a solution that exceeded the benchmark validation set accuracy of 0.93. Although some of the details got lost in the fog of war. I battled with these models for too many days. If you’re curious, you can view a fairly exhaustive list of the models I tested <a href="https://github.com/tommytracey/udacity/blob/master/self-driving-nano/projects/2-traffic-signs/model-performance-summary-v2.xlsx">here</a>.</p><h4>Phase 1</h4><p>The first steps were to get the most basic version of the LeNet CNN running and begin tuning it. I got 83% validation accuracy without any modifications to the model or preprocessing of the training data. Adding regularization and tuning the hyperparameters made the performance worse. So, I started to explore different types of architectures.</p><h4>Phase 2</h4><p>This is where I started making mistakes that cost me a lot of time (although I learned a lot in the process). In hindsight, I should have done two simple things at this point: (1) start applying some basic preprocessing to the data and testing the performance impact, and (2) keep iterating on the LeNet architecture by incrementally adding and deepening the layers.</p><p>Instead, I started exploring different architectures such as <a href="https://arxiv.org/abs/1608.06993">DenseNets</a>. Just look at this diagram from the <a href="https://arxiv.org/abs/1608.06993">DenseNets paper</a> (below).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UTiyprK0BTtDhsRZ.jpg" /></figure><p>How hard could it be, right?</p><p>Wrong.</p><p>DenseNets didn’t seem overly complex at the time, and I probably could have gotten them working if I’d just focused on this. However, in parallel, I tried to get <a href="https://www.tensorflow.org/guide/summaries_and_tensorboard">Tensorboard</a> working, which was in beta at the time. Trying to tackle both of these at once was a disaster. In short, creating DenseNets requires a lot of nested functions to create all of the various blocks of convolutional layers. Getting the Tensorboard namespaces to work, getting all of your variables to initialize properly, and getting all of the data to flow in and out of these blocks was a huge challenge. After a bunch of research and trial and error (and coffee), I ultimately abandoned this path. ¯\_(ツ)_/¯</p><p>I then tried to implement the (much simpler) inception framework discussed by Vincent <a href="https://www.youtube.com/watch?v=SlTm03bEOxA">during the lectures</a>. After some trial and error, I got an inception network running. But, I couldn’t get it to perform better than 80% validation accuracy, so I abandoned this path as well. I believe this approach could have worked, but by this point, I wanted to get back to the basics. So, I decided to focus on data preprocessing and iterating on the original LeNet architecture (which I should have done from the beginning! Arg.)</p><h4>Phase 3</h4><p>After a day of sleep, yoga, and a few dozen ohms to clear my head 🙏…I then got back to work.</p><p>I started by applying basic transformations to the data and testing simple adjustments to the LeNet architecture. Model performance started to improve, but I still had a bias problem. In the beginning, my models were consistently overfitting the training data and therefore my training accuracy was high but my validation accuracy was still low.</p><p>This is a summary of the tactics I deployed to improve performance.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zepaM_UJDvENHiGHNZGckw.png" /></figure><p>Here are more details regarding the tactics above (in order of greatest impact on the model):</p><ul><li><strong>Contrast Enhancement</strong>. Pound for pound, this tactic had the greatest impact on performance. It was easy to implement and my validation accuracy immediately jumped more than 7%. I only wish I’d done it sooner. As discussed in my initial exploration of the data, I predicted that the low contrast of many of the original images would make it difficult for the model to recognize the distinct characteristics of each sign. This is obvious even to the human eye! But for some reason, I didn’t implement this tactic until halfway through the project.<strong> Key lesson: design and test your pipeline around simple observations and intuitions BEFORE you pursue more complicated strategies.</strong></li><li><strong>Augmentation v1 vs v2.</strong> The first iteration of my augmentation function boosted performance by 2% (which was great!). However, my range settings for the affine and color transformations were a little too aggressive. This made the training images overly distorted (this was obvious with the naked eye). Because of these distortions, the model kept overfitting (i.e., it achieved high training accuracy but wasn’t able to generalize to the validation set).</li><li>In v2 of my augmentation function, I dialed down the range settings and got a 1% performance boost. Then I added ZCA whitening to improve edge detection and got another 1% lift. In my very last optimization, I then increased the number of images being produced by this function so that there were 6k images per class (instead of 4k). This tactic combined with longer training time yielded the final (and elusive!) 0.4% lift to bring the final validation accuracy to 99.4%. Then I slept.</li><li><strong>More layers and deeper layers</strong>. Surprisingly, and after many iterations, I learned that it doesn’t take a high number of layers or incredibly deep layers to achieve human-level performance. That said, some modest increases in the size of the model were critical to breaking the 95% accuracy plateau. You can see from the <a href="https://cdn-images-1.medium.com/max/1600/0*LXU6rZlUr2WoeLPp.png">model diagram</a> that I ended up with seven convolutional layers (five more than LeNet) and that my convolutional and fully connected layers are deeper than LeNet as well. Of course, to mitigate this extra learning power, I had to employ regularization tactics.</li><li><strong>Regularization</strong>. Both dropout and L2 regularization proved critical. Initially, I made the mistake of incorporating these too early or setting them too high, which caused the model to underfit. I then removed them altogether until I had a model that was starting to fit (even overfitting) and yielding high training accuracies. At that point, I added regularization back into the model and started to increase it whenever my model was overfitting (i.e., higher dropout and L2 decay values). After a few overcorrections, I ultimately landed on a dropout of 0.5 and decay of 0.0003.</li><li><strong>Bias initialization</strong>. Initially, I was initializing my biases at 0.01 (using tf.constant). Once I started initializing the biases at zero, my accuracy jumped more than 2%. This was a big surprise. Even after doing more research on the issue, I’m still not exactly sure why this small bias initialization negatively affected the model. My best guess is even this small amount of bias was not self-correcting enough during backpropagation, and given that the data was normalized, that extra bias was causing additional overfitting in the model (<a href="https://github.com/tommytracey/udacity/blob/master/self-driving-nano/projects/2-traffic-signs/Traffic_Sign_Classifier_final_v5.py#L576">link to source code</a>).</li><li><strong>Grayscale</strong>. Just out of curiosity, I ran a test on a grayscaled version of the augmented image set. The grayscale set still performed well with a validation accuracy of 95.8%. But, this test turned out to be more trouble than it’s worth. The big problem was that there are a bunch of tools out there to help you convert RGB images to grayscale, and none of them (as far as I can tell) provide the correct shape. To feed grayscale images into the network, they need to be rank 4 (batch_size, 32, 32, 1). So, you have to convert each RGB image from (32, 32, 3) to (32, 32, 1). Seems simple, right? But all of the scripts I tested strip out the third dimension, yielding an image with shape (32, 32). And, there wasn’t much help for this issue on StackOverflow, etc. After lots of troubleshooting, I finally discovered the underlying problem and used a simple matrix multiplication to apply the grayscale conversion while maintaining the right shape (<a href="https://github.com/tommytracey/udacity/blob/master/self-driving-nano/projects/2-traffic-signs/Traffic_Sign_Classifier_final_v5.py#L442">link to source code</a>).</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*IK0AyM38XUjsKThM.jpg" /></figure><h3>Testing the Model with New Images</h3><h4>Test Set</h4><p>I happened to be in Prague at the time, so I took advantage by testing my model on images of local traffic signs (The Czech Republic uses the same traffic signs as Germany and most of the EU, I think). For the test set, I gathered 30 new images: 11 of the images were pulled from the internet, and 19 were shot around the streets of Prague. Overall, I made the new image set quite challenging in order to learn about the strengths and weaknesses of the model.</p><p>Here is the complete set of new images and their corresponding originals.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xBnHMjPhecHZzyEt.png" /></figure><h4>Challenges</h4><p>Within the new image set, the ones below pose distinct challenges for the model. My hypothesis was that the model would get less than 50% of these correct while scoring above 80% on the other “normal” new images. In particular, some of the signs I found on the streets of Prague seem particularly challenging. How would the model react when it sees two standard signs combined into a single custom sign? Keep reading to find out!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/196/1*0YBJ-UIaQfCkpEVCgL__ug.png" /></figure><p>1. <strong>Large Vehicles Prohibited.</strong> Like many signs that I encountered on the streets of Prague, a single traffic sign includes a combination of two or more signs/symbols.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/192/1*B4eWc6HIEDc4Vz5bPZ5r7A.png" /></figure><p>2. <strong>No Trucks or Motorcycles</strong>. Again, what are normally two signs are incorporated into one</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/194/1*gkE7uaXtcv5xGkGaFSCQYA.png" /></figure><p>3. <strong>Yield</strong>. Yet again, the image includes two signs (this one is from the internet)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/190/1*YqdCRTMRRTPdbqoUoLPg6A.png" /></figure><p>4. <strong>No Entry</strong>. The bracket holding up this sign is damaged, so the sign is heavily tilted.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/186/1*NrUSQPKdSeXqbIv3TwVC1w.png" /></figure><p>5. <strong>Turn Right</strong>. This sign is partially occluded by a very pink van.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/186/1*-sCEfP2SR2-4-oKmdU8opQ.png" /></figure><p>6. <strong>50 km/h</strong>. The viewing angle makes the image heavily sheared.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/188/1*oERug_wj5AtPb8lspsaKKQ.png" /></figure><p>7. <strong>No Entry</strong>. This sign has graffiti on it. Those punks!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/182/1*8PAK76vXaPWpgHZyGu1Udg.png" /></figure><p>8. <strong>Ahead Only</strong>. This sign is only partially visible.</p><h4>New Image Test Results</h4><p>The overall accuracy dropped considerably to 77%, although the model performed pretty well on the new images of “normal” difficulty with 91% accuracy. However, this is still well below the 98.2% accuracy achieved on the original test set. This indicates just how quickly accuracy can drop off when a model encounters new patterns it hasn’t yet seen in the training set.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/900/1*6jjp_7kypttT_EkxV08nyw.png" /></figure><h4>Top 5 Predictions</h4><p>Below you can see the top 5 predictions and the corresponding softmax probabilities for a subset of the test images.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/745/0*eK0E3036tWxrQQKb.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/758/0*p8z5ID5c4qtQvA-z.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/722/0*G22l3Fz2DaN02sH6.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/736/0*rWpMKZvEz4l8Hu4F.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/861/0*TBZBz8gklvUm55Y7.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/713/0*eHWiFcITZMOTeQQb.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/760/0*n7RbvCIVf00ld-Zq.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/792/0*BxhARM693Eb-aUDu.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/845/0*HF4UDbPyVAy0lTND.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/716/0*7juiOzs87ZKAamit.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/748/0*iQ5_e3SUShlnbLzE.png" /></figure><h3>Precision &amp; Recall</h3><p>Next, we’ll measure the model accuracy using <a href="https://en.wikipedia.org/wiki/Precision_and_recall">precision and recall</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*s2w0Ha6oSiwLz42c.png" /></figure><h4>Original Test Images</h4><p>Listed below are the precision, recall, and F1 scores for the original set of test images.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/926/0*wMKt4emIZNsEpjgG.jpg" /></figure><p>Here are the worst performing classes among the <strong>original test images</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*P69e5PK9wGjA9T9_4aRFeA.png" /></figure><h4>New Images</h4><p>Here are the worst performing classes for the new image set. Not surprisingly, the worst performing class from the original test set (label 27: Pedestrians) is also one of the poorest performers in the new image set.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*X7c9zOKHw5uDTcI8oUHkYA.png" /></figure><p>There are two things I want to call out here:</p><p><strong>1. High errors for similar looking signs</strong>. If we look at the images from six of the worst performing classes between the two sets, we can see that they all look quite similar. This would help explain the high occurrence of false positives (low precision) and false negatives (low recall). This may also be a case where the transformations done during preprocessing overly distort the images, especially when they’re applied to low-resolution images. The additional loss in resolution can make it hard to distinguish some of the symbols from each other.</p><p>Given this, one future improvement to our pipeline would be to review how each transformation affects the various classes, and if needed, create a custom set of transformations to be applied on a class-by-class basis.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*lsxpHYoJ-aXp0-pF.jpg" /></figure><p><strong>2. Too much augmentation is bad</strong>. I think the most interesting insight from the precision/recall data is the misclassification of the label 15: No Vehicles. If we look at image samples from this class (below), it is arguably the simplest sign and should be one of the easiest to recognize. But upon further inspection, we can see that the contrast boosting function that boosted performance in other classes actually hurts us in this case. This is because any minor spots or shadows on the central white portion of the sign get exacerbated by the contrast enhancement function. These dark spots can then resemble symbols to the model. This is another example of how class-specific preprocessing tactics could improve the pipeline.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*So-HFCG1axh34lBQ.jpg" /></figure><h3>Contact</h3><p>I hope you found this useful. Again, if you have any feedback, I’d love to hear it. Feel free to post in the comments.</p><p>If you’d like to discuss other collaboration or career opportunities you can find me <a href="https://linkedin.com/in/thomastracey">here on LinkedIn</a> or view <a href="https://ttracey.com/">my portfolio here</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=23a4ac66f7a7" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Language Translation with RNNs]]></title>
            <link>https://medium.com/data-science/language-translation-with-rnns-d84d43b40571?source=rss-49cc733eeb2e------2</link>
            <guid isPermaLink="false">https://medium.com/p/d84d43b40571</guid>
            <category><![CDATA[udacity]]></category>
            <category><![CDATA[recurrent-neural-network]]></category>
            <category><![CDATA[language-translation]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[Thomas Tracey]]></dc:creator>
            <pubDate>Sat, 23 Feb 2019 05:57:27 GMT</pubDate>
            <atom:updated>2022-07-23T22:42:35.149Z</atom:updated>
            <content:encoded><![CDATA[<h4>Build a recurrent neural network that translates English to French</h4><p><em>Aug 29, 2018 by </em><a href="https://ttracey.com/"><em>Thomas Tracey</em></a><em>, originally </em><a href="https://github.com/tommytracey/AIND-Capstone/blob/master/README.md"><em>posted on Github</em></a></p><p><a href="https://www.ibidem-translations.com/edu/rnn-machine-translation/"><em>Versión en español </em></a><em>— A Spanish translation of this post is available </em><a href="https://www.ibidem-translations.com/edu/rnn-machine-translation/"><em>here</em></a><em>, courtesy of the </em><a href="https://www.ibidem-translations.com/spanish.php"><em>Ibidem Group</em></a><em>. Muchísimas gracias </em><a href="https://www.linkedin.com/in/josemariabescos/"><em>Chema Bescós</em></a><em>!</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KGcqYRXgidCzctW_.jpg" /><figcaption>Image Credit: Peter Booth and Alexandra Booth / iStock</figcaption></figure><p>This post explores my work on the <a href="https://github.com/tommytracey/AIND-Capstone">final project</a> of the <a href="https://www.udacity.com/course/ai-artificial-intelligence-nanodegree--nd898">Udacity Artificial Intelligence Nanodegree</a> program. <strong>My goal is to help other students and professionals who are in the early phases of building their intuition in machine learning (ML) and artificial intelligence (AI).</strong></p><p>With that said, please keep in mind that I am a product manager by trade (not an engineer or data scientist). So, what follows is meant to be a semi-technical yet approachable explanation of the ML concepts and algorithms in this project. If anything covered below is inaccurate or if you have <em>constructive</em> feedback, I’d love to hear from you.</p><p>My Github repo for this project can be found <a href="https://github.com/tommytracey/AIND-Capstone">here</a>. The original Udacity source repo for this project is located <a href="https://github.com/udacity/aind2-nlp-capstone">here</a>.</p><h3>Project Goal</h3><p>In this project, I build a deep neural network that functions as part of a machine translation pipeline. The pipeline accepts English text as input and returns the French translation. The goal is to achieve the highest translation accuracy possible.</p><h3>Why Machine Translation Matters</h3><p>The ability to communicate with one another is a fundamental part of being human. There are nearly 7,000 different languages worldwide. As our world becomes increasingly connected, language translation provides a critical cultural and economic bridge between people from different countries and ethnic groups. Some of the more obvious use-cases include:</p><ul><li><strong>business</strong>: international trade, investment, contracts, finance</li><li><strong>commerce</strong>: travel, purchase of foreign goods and services, customer support</li><li><strong>media</strong>: accessing information via search, sharing information via social networks, localization of content and advertising</li><li><strong>education</strong>: sharing of ideas, collaboration, translation of research papers</li><li><strong>government</strong>: foreign relations, negotiation</li></ul><p>To meet these needs, technology companies are investing heavily in machine translation. This investment and recent advancements in deep learning have yielded major improvements in translation quality. According to Google, <a href="https://www.washingtonpost.com/news/innovations/wp/2016/10/03/google-translate-is-getting-really-really-accurate">switching to deep learning produced a 60% increase in translation accuracy</a> compared to the phrase-based approach previously used in <a href="https://translate.google.com/">Google Translate</a>. Today, Google and Microsoft can translate over 100 different languages and are approaching human-level accuracy for many of them.</p><p>However, while machine translation has made lots of progress, it’s still not perfect. 😬</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KA769S6-5theK6kA.png" /><figcaption>Bad translation or extreme carnivorism?</figcaption></figure><h3>Approach for This Project</h3><p>To translate a corpus of English text to French, we need to build a recurrent neural network (RNN). Before diving into the implementation, let’s first build some intuition of RNNs and why they’re useful for NLP tasks.</p><h4>RNN Overview</h4><p>RNNs are designed to take sequences of text as inputs or return sequences of text as outputs, or both. They’re called recurrent because the network’s hidden layers have a loop in which the output and cell state from each time step become inputs at the next time step. This recurrence serves as a form of memory. It allows contextual information to flow through the network so that relevant outputs from previous time steps can be applied to network operations at the current time step.</p><p>This is analogous to how we read. As you read this post, you’re storing important pieces of information from previous words and sentences and using it as context to understand each new word and sentence.</p><p>Other types of neural networks can’t do this (yet). Imagine you’re using a convolutional neural network (CNN) to perform object detection in a movie. Currently, there’s no way for information from objects detected in previous scenes to inform the model’s detection of objects in the current scene. For example, if a courtroom and judge were detected in a previous scene, that information could help correctly classify the judge’s gavel in the current scene, instead of misclassifying it as a hammer or mallet. But CNNs don’t allow this type of time-series context to flow through the network like RNNs do.</p><h4>RNN Setup</h4><p>Depending on the use-case, you’ll want to set up your RNN to handle inputs and outputs differently. For this project, we’ll use a many-to-many process where the input is a sequence of English words and the output is a sequence of French words (fourth from the left in the diagram below).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*sm-7smnbyLioThPQ.png" /><figcaption>Diagram of different RNN sequence types. Credit: <a href="http://karpathy.github.io/2015/05/21/rnn-effectiveness/">Andrej Karpathy</a></figcaption></figure><blockquote>Each rectangle is a vector and arrows represent functions (e.g. matrix multiply). Input vectors are in red, output vectors are in blue and green vectors hold the RNN’s state (more on this soon).</blockquote><blockquote>From left to right: (1) Vanilla mode of processing without RNN, from fixed-sized input to fixed-sized output (e.g. image classification). (2) Sequence output (e.g. image captioning takes an image and outputs a sentence of words). (3) Sequence input (e.g. sentiment analysis where a given sentence is classified as expressing positive or negative sentiment). (4) Sequence input and sequence output (e.g. Machine Translation: an RNN reads a sentence in English and then outputs a sentence in French). (5) Synced sequence input and output (e.g. video classification where we wish to label each frame of the video). Notice that in every case are no pre-specified constraints on the lengths sequences because the recurrent transformation (green) is fixed and can be applied as many times as we like.</blockquote><blockquote>— Andrej Karpathy, <a href="http://karpathy.github.io/2015/05/21/rnn-effectiveness/">The Unreasonable Effectiveness of Recurrent Neural Networks</a></blockquote><h3>Building the Pipeline</h3><p>Below is a summary of the various preprocessing and modeling steps. The high-level steps include:</p><ol><li><strong>Preprocessing</strong>: load and examine data, cleaning, tokenization, padding</li><li><strong>Modeling</strong>: build, train, and test the model</li><li><strong>Prediction</strong>: generate specific translations of English to French, and compare the output translations to the ground truth translations</li><li><strong>Iteration</strong>: iterate on the model, experimenting with different architectures</li></ol><p>For a more detailed walkthrough including the source code, check out the <a href="https://github.com/tommytracey/AIND-Capstone/blob/master/machine_translation.ipynb">Jupyter notebook in the project repo</a>.</p><h4>Frameworks</h4><p>We use Keras for the frontend and TensorFlow for the backend in this project. I prefer using Keras on top of TensorFlow because the syntax is simpler, which makes building the model layers more intuitive. However, there is a trade-off with Keras as you lose the ability to do fine-grained customizations. But this won’t affect the models we’re building in this project.</p><h3>Preprocessing</h3><h4><strong>Load &amp; Examine Data</strong></h4><p>Here is a sample of the data. The inputs are sentences in English; the outputs are the corresponding translations in French.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KiND8hSnTJTUh21G.png" /></figure><p>When we run a word count, we can see that the vocabulary for the dataset is quite small. This was by design for this project. This allows us to train the models in a reasonable time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3k_iK2ZmbNsMNlMH.png" /></figure><h4>Cleaning</h4><p>No additional cleaning needs to be done at this point. The data has already been converted to lowercase and split so that there are spaces between all words and punctuation.</p><p><strong>Note</strong>: For other NLP projects you may need to perform additional steps such as: remove HTML tags, remove stop words, remove punctuation or convert to tag representations, label the parts of speech, or perform entity extraction.</p><h4>Tokenization</h4><p>Next, we need to tokenize the data — i.e., convert the text to numerical values. This allows the neural network to perform operations on the input data. For this project, each word and punctuation mark will be given a unique ID. (For other NLP projects, it might make sense to assign each character a unique ID.)</p><p>When we run the tokenizer, it creates a word index, which is then used to convert each sentence to a vector.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0IlrP7F4irUXJGb_.png" /></figure><h4>Padding</h4><p>When we feed our sequences of word IDs into the model, each sequence needs to be the same length. To achieve this, padding is added to any sequence that is shorter than the max length (i.e. shorter than the longest sentence).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/864/0*6jZTOE0P7_i7N8pn.png" /></figure><h4>One-Hot Encoding (not used)</h4><p>In this project, our input sequences will be a vector containing a series of integers. Each integer represents an English word (as seen above). However, in other projects, sometimes an additional step is performed to convert each integer into a one-hot encoded vector. We don’t use one-hot encoding (OHE) in this project, but you’ll see references to it in certain diagrams (like the one below). I just didn’t want you to get confused.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/907/0*buTCMmdernlfWqs0.png" /></figure><p>One of the advantages of OHE is efficiency since it can <a href="https://en.wikipedia.org/wiki/One-hot#cite_note-2">run at a faster clock rate than other encodings</a>. The other advantage is that OHE better represents categorical data where there is no ordinal relationship between different values. For example, let’s say we’re classifying animals as either a mammal, reptile, fish, or bird. If we encode them as 1, 2, 3, 4 respectively, our model may assume there is a natural ordering between them, which there isn’t. It’s not useful to structure our data such that mammal comes before reptile and so forth. This can mislead our model and cause poor results. However, if we then apply one-hot encoding to these integers, changing them to binary representations — 1000, 0100, 0010, 0001 respectively — then no ordinal relationship can be inferred by the model.</p><p>But, one of the drawbacks of OHE is that the vectors can get very long and sparse. The length of the vector is determined by the vocabulary, i.e. the number of unique words in your text corpus. As we saw in the data examination step above, our vocabulary for this project is very small — only 227 English words and 355 French words. By comparison, the <a href="https://en.oxforddictionaries.com/explore/how-many-words-are-there-in-the-english-language/">Oxford English Dictionary has 172,000 words</a>. But, if we include various proper nouns, words tenses, and slang there could be millions of words in each language. For example, <a href="http://mccormickml.com/2016/04/12/googles-pretrained-word2vec-model-in-python/">Google’s word2vec</a> is trained on a vocabulary of 3 million unique words. If we used OHE on this vocabulary, the vector for each word would include one positive value (1) surrounded by 2,999,999 zeros!</p><p>And, since we’re using embeddings (in the next step) to further encode the word representations, we don’t need to bother with OHE. Any efficiency gains aren’t worth it on a data set this small.</p><h3>Modeling</h3><p>First, let’s breakdown the architecture of an RNN at a high level. Referring to the diagram above, there are a few parts of the model we need to be aware of:</p><ol><li><strong>Inputs</strong>. Input sequences are fed into the model with one word for every time step. Each word is encoded as a unique integer or one-hot encoded vector that maps to the English dataset vocabulary.</li><li><strong>Embedding Layers</strong>. Embeddings are used to convert each word to a vector. The size of the vector depends on the complexity of the vocabulary.</li><li><strong>Recurrent Layers (Encoder)</strong>. This is where the context from word vectors in previous time steps is applied to the current word vector.</li><li><strong>Dense Layers (Decoder)</strong>. These are typical fully connected layers used to decode the encoded input into the correct translation sequence.</li><li><strong>Outputs</strong>. The outputs are returned as a sequence of integers or one-hot encoded vectors which can then be mapped to the French dataset vocabulary.</li></ol><h4>Embeddings</h4><p>Embeddings allow us to capture more precise syntactic and semantic word relationships. This is achieved by projecting each word into n-dimensional space. Words with similar meanings occupy similar regions of this space; the closer two words are, the more similar they are. And often the vectors between words represent useful relationships, such as gender, verb tense, or even geopolitical relationships.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*j09oav_Svr3EpZ3n.png" /><figcaption>Photo credit: <a href="https://cbail.github.io/textasdata/word2vec/rmarkdown/word2vec.html">Chris Bail</a></figcaption></figure><p>Training embeddings on a large dataset from scratch requires a huge amount of data and computation. So, instead of doing it ourselves, we normally use a pre-trained embeddings package such as <a href="https://nlp.stanford.edu/projects/glove/">GloVe</a> or <a href="https://mubaris.com/2017/12/14/word2vec/">word2vec</a>. When used this way, embeddings are a form of <a href="https://machinelearningmastery.com/transfer-learning-for-deep-learning/">transfer learning</a>. However, since our dataset for this project has a small vocabulary and low syntactic variation, we’ll use Keras to train the embeddings ourselves.</p><h4>Encoder &amp; Decoder</h4><p>Our sequence-to-sequence model links two recurrent networks: an encoder and decoder. The encoder summarizes the input into a context variable, also called the state. This context is then decoded and the output sequence is generated.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*vqCzgED6tPGQShQf.png" /><figcaption>Image credit: <a href="https://classroom.udacity.com/nanodegrees/nd101/parts/4f636f4e-f9e8-4d52-931f-a49a0c26b710/modules/c1558ffb-9afd-48fa-bf12-b8f29dcb18b0/lessons/43ccf91e-7055-4833-8acc-0e2cf77696e8/concepts/be468484-4bd5-4fb0-82d6-5f5697af07da">Udacity</a></figcaption></figure><p>Since both the encoder and decoder are recurrent, they have loops which process each part of the sequence at different time steps. To picture this, it’s best to unroll the network so we can see what’s happening at each time step.</p><p>In the example below, it takes four timesteps to encode the entire input sequence. At each time step, the encoder “reads” the input word and performs a transformation on its hidden state. Then it passes that hidden state to the next time step. Keep in mind that the hidden state represents the relevant context flowing through the network. The bigger the hidden state, the greater the learning capacity of the model, but also the greater the computation requirements. We’ll talk more about the transformations within the hidden state when we cover gated recurrent units (GRU).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hZ4r1yyTEf2CqoNZ.png" /><figcaption>Image credit: modified version from <a href="https://classroom.udacity.com/nanodegrees/nd101/parts/4f636f4e-f9e8-4d52-931f-a49a0c26b710/modules/c1558ffb-9afd-48fa-bf12-b8f29dcb18b0/lessons/43ccf91e-7055-4833-8acc-0e2cf77696e8/concepts/f999d8f6-b4c1-4cd0-811e-4767b127ae50">Udacity</a></figcaption></figure><p>For now, notice that for each time step after the first word in the sequence there are two inputs: the hidden state and a word from the sequence. For the encoder, it’s the <em>next</em> word in the input sequence. For the decoder, it’s the <em>previous</em> word from the output sequence.</p><p>Also, remember that when we refer to a “word,” we really mean the <em>vector representation</em> of the word which comes from the embedding layer.</p><p>Here’s another way to visualize the encoder and decoder, except with a Mandarin input sequence.</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/0*9KfkdXqz-ZHxe3eF.gif" /><figcaption>Image credit: <a href="https://xiandong79.github.io/seq2seq-%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86">xiandong79.github.io</a></figcaption></figure><h4>Bidirectional Layer</h4><p>Now that we understand how context flows through the network via the hidden state, let’s take it a step further by allowing that context to flow in both directions. This is what a bidirectional layer does.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/0*DHjnkS0rueJik4R0.jpg" /></figure><p>In the example above, the encoder only has historical context. But, providing future context can result in better model performance. This may seem counterintuitive to the way humans process language since we only read in one direction. However, humans often require future context to interpret what is being said. In other words, sometimes we don’t understand a sentence until an important word or phrase is provided at the end. Happens this does whenever Yoda speaks. 😑 🙏</p><p>To implement this, we train two RNN layers simultaneously. The first layer is fed the input sequence as-is and the second is fed a reversed copy.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/836/0*IYLXY8Dxvw8rbd8E.png" /><figcaption>Image credit: Udacity</figcaption></figure><h4>Hidden Layer with Gated Recurrent Unit (GRU)</h4><p>Now let’s make our RNN a little bit smarter. Instead of allowing <em>all</em> of the information from the hidden state to flow through the network, what if we could be more selective? Perhaps some of the information is more relevant, while other information should be discarded. This is essentially what a gated recurrent unit (GRU) does.</p><p>There are two gates in a GRU: an update gate and reset gate. <a href="https://towardsdatascience.com/understanding-gru-networks-2ef37df6c9be">This article</a> by Simeon Kostadinov, explains these in detail. To summarize, the <strong>update gate </strong>(z) helps the model determine how much information from previous time steps needs to be passed along to the future. Meanwhile, the <strong>reset gate</strong> (r) decides how much of the past information to forget.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*FvPLILqWS2FWD-98.png" /><figcaption>Image Credit: <a href="https://www.analyticsvidhya.com/blog/2017/12/introduction-to-recurrent-neural-networks/gru/">analyticsvidhya.com</a></figcaption></figure><h4>Final Model</h4><p>Now that we’ve discussed the various parts of our model, let’s take a look at the code. Again, all of the source code is available <a href="https://github.com/tommytracey/AIND-Capstone/blob/master/machine_translation.ipynb">here in the notebook</a> (<a href="https://tommytracey.github.io/AIND-Capstone/machine_translation.html">.html version</a>).</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6bddbbc58b75c6fc5b0d8fcb1ea5a369/href">https://medium.com/media/6bddbbc58b75c6fc5b0d8fcb1ea5a369/href</a></iframe><h3>Results</h3><p>The results from the final model can be found in cell 20 of the <a href="https://tommytracey.github.io/AIND-Capstone/machine_translation.html">notebook</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MkdtK5JXvYNFHFXHCPU3aw.png" /></figure><p>Validation accuracy: 97.5%</p><p>Training time: 23 epochs</p><h3>Future Improvements</h3><ol><li><strong>Do proper data split (training, validation, test)</strong>. Currently, there is no test set, only training and validation. Obviously, this doesn’t follow best practices.</li><li><strong>LSTM + attention</strong>. This has been the de facto architecture for RNNs over the past few years, although there are <a href="https://towardsdatascience.com/the-fall-of-rnn-lstm-2d1594c74ce0">some limitations</a>. I didn’t use LSTM because I’d <a href="https://github.com/tommytracey/udacity/tree/master/deep-learning-nano/projects/4-language-translation#build-the-neural-network">already implemented it in TensorFlow in another project</a>, and I wanted to experiment with GRU + Keras for this project.</li><li><strong>Train on a larger and more diverse text corpus</strong>. The text corpus and vocabulary for this project are quite small with little variation in syntax. As a result, the model is very brittle. To create a model that generalizes better, you’ll need to train on a larger dataset with more variability in grammar and sentence structure.</li><li><strong>Residual layers</strong>. You could add residual layers to a deep LSTM RNN, as described in <a href="https://arxiv.org/abs/1701.03360">this paper</a>. Or, use residual layers as an alternative to LSTM and GRU, as described <a href="http://www.mdpi.com/2078-2489/9/3/56/pdf">here</a>.</li><li><strong>Embeddings</strong>. If you’re training on a larger dataset, you should definitely use a pre-trained set of embeddings such as <a href="https://mubaris.com/2017/12/14/word2vec/">word2vec</a> or <a href="https://nlp.stanford.edu/projects/glove/">GloVe</a>. Even better, use ELMo or BERT.</li></ol><ul><li><strong>Embedding Language Model (ELMo)</strong>. One of the biggest advances in <a href="https://medium.com/huggingface/universal-word-sentence-embeddings-ce48ddc8fc3a">universal embeddings</a> in 2018 was <a href="https://allennlp.org/elmo">ELMo</a>, developed by the <a href="https://allennlp.org/">Allen Institute for AI</a>. One of the major advantages of ELMo is that it addresses the problem of polysemy, in which a single word has multiple meanings. ELMo is context-based (not word-based), so different meanings for a word occupy different vectors within the embedding space. With GloVe and word2vec, each word has only one representation in the embedding space. For example, the word “queen” could refer to the matriarch of a royal family, a bee, a chess piece, or the 1970s rock band. With traditional embeddings, all of these meanings are tied to a single vector for the word <em>queen</em>. With ELMO, these are four distinct vectors, each with a unique set of context words occupying the same region of the embedding space. For example, we’d expect to see words like <em>queen</em>, <em>rook</em>, and <em>pawn</em> in a similar vector space related to the game of chess. And we’d expect to see <em>queen</em>, <em>hive</em>, and <em>honey</em> in a different vector space related to bees. This provides a significant boost in semantic encoding.</li><li><strong>Bidirectional Encoder Representations from </strong><a href="https://ai.googleblog.com/2017/08/transformer-novel-neural-network.html"><strong>Transformers</strong></a><strong> (BERT)</strong>. So far in 2019, the biggest advancement in bidirectional embeddings has been <a href="https://ai.googleblog.com/2018/11/open-sourcing-bert-state-of-art-pre.html">BERT</a>, which was open-sourced by Google. How is BERT different?</li></ul><blockquote>Context-free models such as word2vec or GloVe generate a single word embedding representation for each word in the vocabulary. For example, the word “bank” would have the same context-free representation in “bank account” and “bank of the river.” Contextual models instead generate a representation of each word that is based on the other words in the sentence. For example, in the sentence “I accessed the bank account,” a unidirectional contextual model would represent “bank” based on “I accessed the” but not “account.” However, BERT represents “bank” using both its previous and next context — “I accessed the … account” — starting from the very bottom of a deep neural network, making it deeply bidirectional.<br> — Jacob Devlin and Ming-Wei Chang, <a href="https://ai.googleblog.com/2018/11/open-sourcing-bert-state-of-art-pre.html">Google AI Blog</a></blockquote><h3>Contact</h3><p>I hope you found this useful. Again, if you have any feedback, I’d love to hear it. Feel free to post in the comments.</p><p>If you’d like to discuss other collaboration or career opportunities you can find me <a href="https://linkedin.com/in/thomastracey">here on LinkedIn</a> or view <a href="https://ttracey.com/">my portfolio here</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d84d43b40571" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/language-translation-with-rnns-d84d43b40571">Language Translation with RNNs</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[Training Two Agents to Play Tennis]]></title>
            <link>https://medium.com/data-science/training-two-agents-to-play-tennis-8285ebfaec5f?source=rss-49cc733eeb2e------2</link>
            <guid isPermaLink="false">https://medium.com/p/8285ebfaec5f</guid>
            <category><![CDATA[udacity]]></category>
            <category><![CDATA[reinforcement-learning]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <dc:creator><![CDATA[Thomas Tracey]]></dc:creator>
            <pubDate>Fri, 22 Feb 2019 23:16:53 GMT</pubDate>
            <atom:updated>2019-02-25T07:43:56.452Z</atom:updated>
            <content:encoded><![CDATA[<h3>Training Bots to Play Tennis</h3><h4>Deep Reinforcement Learning for Multi-Agent Collaboration &amp; Competition</h4><p><em>Dec 26, 2018 by </em><a href="https://ttracey.com/"><em>Thomas Tracey</em></a><em>, originally </em><a href="https://github.com/tommytracey/DeepRL-P3-Collaboration-Competition/blob/master/README.md"><em>posted on Github</em></a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/0*4jb5ipeA2y_xc3Wy.gif" /><figcaption>Photo credit: <a href="https://www.youtube.com/watch?v=imVNg9j7rvU">Ulf Hoffmann</a></figcaption></figure><p>This post explores my work on the <a href="https://github.com/tommytracey/DeepRL-P3-Collaboration-Competition">final project</a> for Udacity’s <a href="https://www.udacity.com/course/deep-reinforcement-learning-nanodegree--nd893">Deep Reinforcement Learning Nanodegree</a>. <strong>My goal is to help other machine learning (ML) students and professionals who are in the early phases of building their intuition in reinforcement learning (RL).</strong></p><p>With that said, please keep in mind that I am a product manager by trade (not an engineer or data scientist). So, what follows is meant to be a semi-technical yet approachable explanation of the RL concepts and algorithms in this project. If anything covered below is inaccurate, or if you have constructive feedback, I’d love to hear it.</p><p>My Github repo for this project can be found <a href="https://github.com/tommytracey/DeepRL-P3-Collaboration-Competition">here</a>. The original Udacity source repo for this project is located <a href="https://github.com/udacity/deep-reinforcement-learning/tree/master/p3_collab-compet">here</a>.</p><h3>Big Picture: Why RL Matters</h3><p>For artificial intelligence (AI) to reach its full potential, <strong>AI systems need to interact effectively with humans and with other agents</strong>. There are already environments where this type of agent-human and agent-agent interaction is happening on a massive scale, such as the stock market. And more environments are just around the corner, such as our roadways once fully autonomous vehicles are introduced.</p><p>One step along this path is to train AI agents to interact with other agents in both cooperative and competitive settings. <a href="https://skymind.ai/wiki/deep-reinforcement-learning">Reinforcement learning (RL)</a> is a subfield of AI that’s shown promise. RL is distinct from other types of machine learning approaches in that RL agents are goal-oriented. That is, RL algorithms use a reward function (aka an objective function) to “reinforce” certain behaviors. For example, a Pac-Man agent may earn a reward of +1 points for each pellet it eats and -100 points if it collides with a ghost. By trying to maximize its cumulative reward, the Pac-Man agent ultimately learns — through repeated trial-and-error — how to distinguish good actions from bad actions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*CyfNg5FAEHbO9Cwm.gif" /><figcaption>Image credit: <a href="https://www.pixilart.com/art/pacman-gif-dae813cd8dfeede">pixelart.com</a></figcaption></figure><p>OpenAI has made a <a href="https://gym.openai.com/envs/#atari">large set of virtual environments</a> publicly available for training and benchmarking RL agents. This has spurred lots of research and innovation in RL. However, much of the initial progress was in single-agent domains, where building models that predict the behavior of other actors isn’t necessary. As a result, traditional RL approaches (such as Q-Learning) are not well-suited for the complexity that accompanies environments where multiple agents are continuously interacting and evolving their policies.</p><blockquote>Unfortunately, traditional reinforcement learning approaches such as Q-Learning or policy gradient are poorly suited to multi-agent environments. One issue is that each agent’s policy is changing as training progresses, and the environment becomes non-stationary from the perspective of any individual agent in a way that is not explainable by changes in the agent’s own policy. This presents learning stability challenges and prevents the straightforward use of past experience replay, which is crucial for stabilizing deep Q-learning. Policy gradient methods, on the other hand, usually exhibit very high variance when coordination of multiple agents is required. Alternatively, one can use model-based policy optimization which can learn optimal policies via back-propagation, but this requires a differentiable model of the world dynamics and assumptions about the interactions between agents. Applying these methods to competitive environments is also challenging from an optimization perspective, as evidenced by the notorious instability of adversarial training methods.</blockquote><blockquote>— Lowe and Wu et al, <a href="https://papers.nips.cc/paper/7217-multi-agent-actor-critic-for-mixed-cooperative-competitive-environments.pdf">Multi-Agent Actor-Critic for Mixed Cooperative-Competitive Environments</a></blockquote><p>Wow, that’s a mouthful. Essentially, what’s needed (and what’s explored in this project) is a general framework that allows multiple agents to learn from their own observations in both cooperative and competitive environments. This is without any communication between the agents or modeling of other agents’ behaviors. However, each agent in this project does learn by observing its own actions and the actions of the other agent. Each observation includes: the prior game state, the agent’s action, the reward, and the subsequent game state (s, a, r, sʹ).</p><p>New approaches for cooperative and competitive environments are advancing quickly. I won’t explore them further in this post, but I recommend checking out <a href="https://openai.com/five/">OpenAI Five</a> and <a href="https://deepmind.com/blog/alphastar-mastering-real-time-strategy-game-starcraft-ii/">DeepMind AlphaStar</a>.</p><h3>Goal of this Project</h3><p>The goal of this project is to train two RL agents to play tennis. As in real tennis, the goal of each player is to keep the ball in play. And, when you have two equally matched opponents, you tend to see fairly long exchanges where the players hit the ball back and forth over the net.</p><h3>The Environment</h3><p>We’ll work with an environment that is similar, but not identical to the <a href="https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Learning-Environment-Examples.md#tennis">Tennis environment</a> on the Unity ML-Agents GitHub page.</p><p>In this environment, two agents control rackets to bounce a ball over a net. If an agent hits the ball over the net, it receives a reward of +0.1. If an agent lets a ball hit the ground or hits the ball out of bounds, it receives a reward of -0.01. Thus, the goal of each agent is to keep the ball in play.</p><p>The observation space consists of 8 variables corresponding to the position and velocity of the ball and racket. Each agent receives its own, local observation. Two continuous actions are available, corresponding to moves toward (or away from) the net, and jumping.</p><p>The task is episodic, and in order to solve the environment, the agents must get an average score of +0.5 (over 100 consecutive episodes, after taking the maximum over both agents).</p><p>Specifically,</p><ul><li>After each episode, we add up the rewards that each agent received (without discounting), to get a score for each agent. This yields 2 potentially different scores. We then take the maximum of these 2 scores.</li><li>This yields a single score for each episode.</li><li>The environment is considered solved when the average (over 100 episodes) of those scores is at least +0.5.</li></ul><p>Here is an example of two semi-trained agents interacting in this environment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/0*vMejAJQGj_vxZIu6.gif" /></figure><h3>Approach</h3><p>Here are the high-level steps that were taken in building an agent that solves this environment.</p><ol><li>Establish performance baseline using a random action policy.</li><li>Select an appropriate algorithm and begin implementing it.</li><li>Run experiments, make revisions, and retrain the agent until the performance threshold is reached.</li></ol><p>DISCLAIMER: I <em>ultimately</em> reached a good solution; however, the results were not consistent. My “best” results were only reproducible if I reran the model numerous times (&gt;10). If you just run the model once (or even 3–5 times), it might not converge. And, during the initial implementation, I ran the model at least 30 times while searching for a reliable set of hyperparameters. If you want to experiment with different approaches, I strongly recommend implementing a more <a href="https://en.wikipedia.org/wiki/Hyperparameter_optimization#Approaches">systematic approach to hyperparameter tuning</a> (which I did not do, but wish I had).</p><h3>Establishing a Baseline</h3><p>Before building agents that learn, I started by testing ones that select actions (uniformly) at random at each time step.</p><p>Running the random agents a few times resulted in scores from 0 to 0.02. Obviously, if these agents need to achieve an average score of 0.5 over 100 consecutive episodes, then choosing actions at random won’t work. However, when you watch the agents acting randomly, it becomes clear that these types of sporadic actions can be useful early in the training process. That is, they can help the agents explore the action space to find some signal of good vs. bad actions. This insight will come into play later when we implement the Ornstein-Uhlenbeck process and epsilon noise decay.</p><h3>Implementing the Learning Algorithm</h3><p>To get started, there are a few high-level architecture decisions we need to make. First, we need to determine which types of algorithms are most suitable for the Tennis environment.</p><h4>Policy-based vs Value-based Methods</h4><p>There are two key differences in the Tennis environment compared to the ‘Navigation’ environment from the <a href="https://github.com/tommytracey/DeepRL-P1-Navigation">first project in Udacity’s Deep RL program</a>:</p><ol><li><strong>Multiple agents</strong>. The Tennis environment has two different agents, whereas the Navigation project had only a single agent.</li><li><strong>Continuous action space</strong>. The action space is now <em>continuous</em> (instead of <em>discrete</em>), which allows each agent to execute more complex and precise movements. Even though each tennis agent can only move forward, backward, or jump, there’s an unlimited range of possible action values that control these movements. Whereas, the agent in the <a href="https://github.com/tommytracey/DeepRL-P1-Navigation">Navigation project</a> was limited to four <em>discrete</em> actions: left, right, forward, backward.</li></ol><p>Given the additional complexity of this environment, the <em>value-based method</em> we used for the Navigation project is not suitable — i.e., the Deep Q-Network (DQN) algorithm. Most importantly, we need an algorithm that allows the tennis agent to utilize its full range and power of movement. For this, we’ll need to explore a different class of algorithms called <em>policy-based methods</em>.</p><p>Here are some advantages of policy-based methods:</p><ul><li><strong>Continuous action spaces. </strong>Policy-based methods are well-suited for continuous action spaces.</li><li><strong>Stochastic policies</strong>. Both value-based and policy-based methods can learn deterministic policies. However, policy-based methods can also learn true stochastic policies.</li><li><strong>Simplicity</strong>. Policy-based methods directly learn the optimal policy, without having to maintain a separate value function estimate. With value-based methods, the agent uses its experience with the environment to maintain an estimate of the optimal action-value function, from which an optimal policy is derived. This intermediate step requires the storage of lots of additional data since you need to account for all possible action values. Even if you discretize the action space, the number of possible actions can get quite large. And, using DQN to determine the action that maximizes the action-value function within a continuous or high-dimensional space requires a complex optimization process at every timestep.</li></ul><h4>Multi-Agent Deep Deterministic Policy Gradient (MADDPG)</h4><p>The original DDPG algorithm, which I extended to create the MADDPG version, is outlined in <a href="https://arxiv.org/pdf/1509.02971.pdf">this paper</a>, <em>Continuous Control with Deep Reinforcement Learning</em>, by researchers at Google Deepmind. In this paper, the authors present “a model-free, off-policy actor-critic algorithm using deep function approximators that can learn policies in high-dimensional, continuous action spaces.” They highlight that DDPG can be viewed as an extension of Deep Q-learning to continuous tasks.</p><p>For the DDPG foundation, I used <a href="https://github.com/udacity/deep-reinforcement-learning/tree/master/ddpg-pendulum">this vanilla, single-agent DDPG</a> as a template. Then, to make this algorithm suitable for the multiple competitive agents in the Tennis environment, I implemented components discussed in <a href="https://papers.nips.cc/paper/7217-multi-agent-actor-critic-for-mixed-cooperative-competitive-environments.pdf">this paper</a>, <em>Multi-Agent Actor-Critic for Mixed Cooperative-Competitive Environments</em>, by Lowe and Wu, along with other researchers from OpenAI, UC Berkeley, and McGill University. Most notably, I implemented their variation of the actor-critic method (see Figure 1), which I discuss in the following section.</p><p>Lastly, I further experimented with components of the DDPG algorithm based on other concepts covered in Udacity’s classroom and lessons. My implementation of this algorithm (including various customizations) is discussed below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/858/0*oILRAhHg7rm5K7rR.png" /><figcaption><strong>Figure 1</strong>: Multi-agent decentralized actor with centralized critic (<a href="https://papers.nips.cc/paper/7217-multi-agent-actor-critic-for-mixed-cooperative-competitive-environments.pdf">Lowe and Wu et al</a>).</figcaption></figure><h4>Actor-Critic Method</h4><p>Actor-critic methods leverage the strengths of both policy-based and value-based methods.</p><p>Using a policy-based approach, the agent (actor) learns how to act by directly estimating the optimal policy and maximizing reward through gradient ascent. Meanwhile, employing a value-based approach, the agent (critic) learns how to estimate the value (i.e., the future cumulative reward) of different state-action pairs. Actor-critic methods combine these two approaches in order to accelerate the learning process. Actor-critic agents are also more stable than value-based agents, while requiring fewer training samples than policy-based agents.</p><p>What makes this implementation unique is the <em>decentralized actor with centralized critic</em> approach from <a href="https://papers.nips.cc/paper/7217-multi-agent-actor-critic-for-mixed-cooperative-competitive-environments.pdf">the paper by Lowe and Wu</a>. Whereas traditional actor-critic methods have a separate critic for each agent, this approach utilizes a single critic that receives as input the actions and state observations from all agents. This extra information makes training easier and allows for centralized training with decentralized execution. Each agent still takes actions based on its own unique observations of the environment.</p><p>You can find the actor-critic logic implemented as part of the Agent() class <a href="https://github.com/tommytracey/DeepRL-P3-Collaboration-Competition/blob/master/maddpg_agent.py#L110">here</a> in maddpg_agent.py of the source code. The actor-critic models can be found via their respective Actor() and Critic() classes <a href="https://github.com/tommytracey/DeepRL-P3-Collaboration-Competition/blob/master/model.py#L12">here</a> in models.py.</p><p>Note: As we did with Double Q-Learning in the last project, <a href="https://github.com/tommytracey/DeepRL-P2-Continuous-Control">Continuous Control: Training a Set of Robotic Arms</a>, we’re again leveraging local and target networks to improve stability. This is where one set of parameters <em>w</em> is used to select the best action, and another set of parameters <em>wʹ</em> is used to evaluate that action. In this project, local and target networks are implemented separately for both the actor and the critic.</p><h4>Exploration vs Exploitation</h4><p>One challenge is choosing which action to take while the agent is still learning the optimal policy. Should the agent choose an action based on the rewards observed thus far? Or, should the agent try a new action in hopes of earning a higher reward? This is known as the <em>exploration vs. exploitation</em> dilemma.</p><p>In the previous <a href="https://github.com/tommytracey/DeepRL-P1-Navigation">Navigation project</a>, I addressed this by implementing an <a href="https://github.com/tommytracey/DeepRL-P1-Navigation/blob/master/agent.py#L80">𝛆-greedy algorithm</a>. This algorithm allows the agent to systematically manage the exploration vs. exploitation trade-off. The agent “explores” by picking a random action with some probability epsilon 𝛜. Meanwhile, the agent continues to “exploit” its knowledge of the environment by choosing actions based on the deterministic policy with probability (1-𝛜).</p><p>However, this approach won’t work for controlling the tennis agents. The reason is that the actions are no longer a discrete set of simple directions (i.e., up, down, left, right). The actions driving the movement of the arm are forces with different magnitudes and directions. If we base our exploration mechanism on random uniform sampling, the direction actions would have a mean of zero, in turn canceling each other out. This can cause the system to oscillate without making much progress.</p><p>Instead, we’ll use the Ornstein-Uhlenbeck process, as suggested in the <a href="https://arxiv.org/pdf/1509.02971.pdf">previously mentioned paper by Google DeepMind</a> (see bottom of page 4). The Ornstein-Uhlenbeck process adds a certain amount of noise to the action values at each timestep. This noise is correlated to previous noise and therefore tends to stay in the same direction for longer durations without canceling itself out. This allows the agent to maintain velocity and explore the action space with more continuity.</p><p>You can find the Ornstein-Uhlenbeck process implemented <a href="https://github.com/tommytracey/DeepRL-P3-Collaboration-Competition/blob/master/maddpg_agent.py#L167">here</a> in the OUNoise() class in maddpg_agent.py of the source code.</p><p>In total, there are five hyperparameters related to this noise process.</p><p>The Ornstein-Uhlenbeck process itself has three hyperparameters that determine the noise characteristics and magnitude:</p><ul><li><strong><em>mu</em></strong>: the long-running mean</li><li><strong><em>theta</em></strong>: the speed of mean reversion</li><li><strong><em>sigma</em></strong>: the volatility parameter</li></ul><p>Of these, I only tuned sigma. After running a few experiments, I reduced sigma from 0.3 to 0.2. The reduced noise volatility seemed to help the model converge.</p><p>Notice also there’s an epsilon parameter used to decay the noise level over time. This decay mechanism ensures that more noise is introduced earlier in the training process (i.e., higher exploration), and the noise decreases over time as the agent gains more experience (i.e., higher exploitation). The starting value for epsilon and its decay rate are two hyperparameters that were tuned during experimentation.</p><p>You can find the epsilon decay process implemented <a href="https://github.com/tommytracey/DeepRL-P3-Collaboration-Competition/blob/master/maddpg_agent.py#L79">here</a> in the Agent.act() method in maddpg_agent.py of the source code. While the epsilon decay is performed <a href="https://github.com/tommytracey/DeepRL-P3-Collaboration-Competition/blob/master/maddpg_agent.py#L150">here</a> as part of the learning step.</p><p>The final noise parameters were set as follows:</p><pre>OU_SIGMA = 0.2   # Ornstein-Uhlenbeck noise parameter, volatility<br>OU_THETA = 0.15  # OU noise parameter, speed of mean reversion<br>EPS_START = 5.0  # initial value for epsilon in noise decay process<br>EPS_EP_END = 300 # episode to end the noise decay process<br>EPS_FINAL = 0    # final value for epsilon after decay</pre><p><strong>IMPORTANT NOTE</strong>: Notice that the EPS_START parameter is set at 5.0. For dozens of experiments, I had this parameter set to 1.0, as I had in previous projects. But, I had a difficult time getting the model to converge, and if it did, it converged very slowly (&gt;1500 episodes). After much trial and error, I realized that the agents had some difficulty discovering signal early in the process (i.e., most episode scores equaled zero). By boosting the noise output from the Ornstein-Uhlenbeck (OU) process, it encouraged aggressive exploration of the action space and therefore improved the chances that signal would be detected (i.e., making contact with the ball). This extra signal seemed to improve learning later in training once the noise decayed to zero.</p><h4>Learning Interval</h4><p>In the first few versions of my implementation, the agent only performed a single learning iteration per episode. Although the best model had this setting, this seemed to be a stroke of luck. In general, I found that performing multiple learning passes per episode yielded faster convergence and higher scores. This did make training slower, but it was a worthwhile trade-off. In the end, I implemented an interval in which the learning step is performed every episode. As part of each learning step, the algorithm then samples experiences from the buffer and runs the Agent.learn() method 10 times.</p><pre>LEARN_EVERY = 1  # learning interval (no. of episodes)<br>LEARN_NUM = 5    # number of passes per learning step</pre><p>You can find the learning interval implemented <a href="https://github.com/tommytracey/DeepRL-P3-Collaboration-Competition/blob/master/maddpg_agent.py#L67">here</a> in the Agent.step() method in maddpg_agent.py of the source code.</p><h4>Gradient Clipping</h4><p>In early versions of my implementation, I had trouble getting my agent to learn. Or, rather, it would start to learn but then become very unstable and either plateau or collapse.</p><p>I suspect that one of the causes was outsized gradients. Unfortunately, I couldn’t find an easy way to investigate this, although I’m sure there’s some way of doing this in PyTorch. Absent this investigation, I hypothesize that many of the weights from my critic model were becoming quite large after just 50–100 episodes of training. And since I was running the learning process multiple times per episode, it only made the problem worse.</p><p>The issue of exploding gradients is described in layman’s terms in <a href="https://machinelearningmastery.com/exploding-gradients-in-neural-networks">this post by Jason Brownlee</a>. Essentially, each layer of your net amplifies the gradient it receives. This becomes a problem when the lower layers of the network accumulate huge gradients, making their respective weight updates too large to allow the model to learn anything.</p><p>To combat this, I implemented gradient clipping using the <a href="https://pytorch.org/docs/stable/_modules/torch/nn/utils/clip_grad.html">torch.nn.utils.clip_grad_norm</a> function. I set the function to “clip” the norm of the gradients at 1, therefore placing an upper limit on the size of the parameter updates, and preventing them from growing exponentially. Once this change was implemented, my model became much more stable and my agent started learning at a much faster rate.</p><p>You can find gradient clipping implemented <a href="https://github.com/tommytracey/DeepRL-P3-Collaboration-Competition/blob/master/maddpg_agent.py#L128">here</a> in the “update critic” section of the Agent.learn() method, within ddpg_agent.py of the source code.</p><p>Note that this function is applied after the backward pass but before the optimization step.</p><pre># Compute critic loss<br>Q_expected = self.critic_local(states, actions)<br>critic_loss = F.mse_loss(Q_expected, Q_targets)</pre><pre># Minimize the loss<br>self.critic_optimizer.zero_grad()<br>critic_loss.backward()<br>torch.nn.utils.clip_grad_norm_(self.critic_local.parameters(), 1)<br>self.critic_optimizer.step()</pre><h4>Experience Replay</h4><p>Experience replay allows the RL agent to learn from past experience.</p><p>As with the <a href="https://github.com/tommytracey/DeepRL-P2-Continuous-Control">previous project</a>, the algorithm employs a replay buffer to gather experiences. Experiences are stored in a single replay buffer as each agent interacts with the environment. These experiences are then utilized by the central critic, therefore allowing the agents to learn from each others’ experiences.</p><p>The replay buffer contains a collection of experience tuples with the state, action, reward, and next state (s, a, r, sʹ). The critic samples from this buffer as part of the learning step. Experiences are sampled randomly so that the data is uncorrelated. This prevents action values from oscillating or diverging catastrophically since a naive algorithm could otherwise become biased by correlations between sequential experience tuples.</p><p>Also, experience replay improves learning through repetition. By doing multiple passes over the data, our agents have multiple opportunities to learn from a single experience tuple. This is particularly useful for state-action pairs that occur infrequently within the environment.</p><p>The implementation of the replay buffer can be found <a href="https://github.com/tommytracey/DeepRL-P3-Collaboration-Competition/blob/master/maddpg_agent.py#L196">here</a> in the maddpg_agent.py file of the source code.</p><h3>Results</h3><p>Once all of the above components were in place, the agents were able to solve the Tennis environment. Again, the performance goal is an average reward of at least +0.5 over 100 episodes, taking the best score from either agent for a given episode.</p><p><a href="https://youtu.be/jOWWzygOi1A">Here</a> is a video showing the trained agents playing a few points.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FjOWWzygOi1A%3Ffeature%3Doembed&amp;url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DjOWWzygOi1A&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FjOWWzygOi1A%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/352a961d2c38bafb373886392accef17/href">https://medium.com/media/352a961d2c38bafb373886392accef17/href</a></iframe><p>The graph below shows the final training results. The best-performing agents were able to solve the environment in 607 episodes, with a top score of 5.2 and a top moving average of 0.927. The complete set of results and steps can be found in the <a href="https://github.com/tommytracey/DeepRL-P3-Collaboration-Competition/blob/master/Tennis.ipynb">Tennis.ipynb</a> Jupyter notebook.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/380/1*NmB3PJCDMhfVwethZqd10g.png" /></figure><h4>Future Improvements</h4><ul><li><strong>Address stability issues to produce more consistent results</strong>. My “best” results are only reproducible if you run the model numerous times. If you just run it once (or even 3–5 times) the model might not converge. I ran the model at least 30 while searching for a good set of hyperparameters, so perhaps implementing a more systemic approach such as grid search would help. Otherwise, more research is needed to find a more stable algorithm or to make changes to the current DDPG algorithm.</li><li><strong>Add <em>prioritized</em> experience replay</strong>. Rather than selecting experience tuples randomly, prioritized replay selects experiences based on a priority value that is correlated with the magnitude of the error. This can improve learning by increasing the probability that rare or important experience vectors are sampled.</li><li><strong>Batch Normalization</strong>. I did not use batch normalization on this project, but I probably should have. I’ve used batch normalization many times in the past when building convolutional neural networks (CNN), in order to squash pixel values. But, it didn’t occur to me that it would be to this project. Batch normalization was used in the <a href="https://arxiv.org/pdf/1509.02971.pdf">Google DeepMind paper</a> and has proved tremendously useful in my implementation of other projects. Similar to the exploding gradient issue mentioned above, running computations on large input values and model parameters can inhibit learning. Batch normalization addresses this problem by scaling the features to be within the same range throughout the model and across different environments and units. In additional to normalizing each dimension to have unit mean and variance, the range of values is often much smaller, typically between 0 and 1. You can find batch normalization implemented <a href="https://github.com/tommytracey/DeepRL-P2-Continuous-Control/blob/master/model.py#L41">here</a> for the actor and <a href="https://github.com/tommytracey/DeepRL-P2-Continuous-Control/blob/master/model.py#L75">here</a> for the critic, within model.py of the source code of my <em>previous</em> project, <a href="https://github.com/tommytracey/DeepRL-P2-Continuous-Control/">Training Robotic Arms with Continuous Control</a>. This greatly improved model performance and stability.</li></ul><h3>Contact</h3><p>I hope you found this useful. Again, if you have any feedback, I’d love to hear it. Feel free to post in the comments.</p><p>If you’d like to discuss other collaboration or career opportunities you can find me <a href="https://linkedin.com/in/thomastracey">here on LinkedIn</a> or view <a href="https://ttracey.com/">my portfolio here</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8285ebfaec5f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/training-two-agents-to-play-tennis-8285ebfaec5f">Training Two Agents to Play Tennis</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>