<?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 Elad Rapaport on Medium]]></title>
        <description><![CDATA[Stories by Elad Rapaport on Medium]]></description>
        <link>https://medium.com/@erap129?source=rss-d2d1ff8f0490------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*0Z5jwnh5SpZ6EBDR1Qk81g.jpeg</url>
            <title>Stories by Elad Rapaport on Medium</title>
            <link>https://medium.com/@erap129?source=rss-d2d1ff8f0490------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 21:25:36 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@erap129/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[Back(prop) to Basics — Fundamentals of Neural Networks]]></title>
            <link>https://medium.com/@erap129/back-prop-to-basics-fundamentals-of-neural-networks-d54cd8fc61e4?source=rss-d2d1ff8f0490------2</link>
            <guid isPermaLink="false">https://medium.com/p/d54cd8fc61e4</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[neural-networks]]></category>
            <dc:creator><![CDATA[Elad Rapaport]]></dc:creator>
            <pubDate>Fri, 26 May 2023 13:49:57 GMT</pubDate>
            <atom:updated>2023-05-26T13:49:57.898Z</atom:updated>
            <content:encoded><![CDATA[<h3>Back(prop) to Basics — Fundamentals of Neural Networks</h3><p>Andrej Karpathy’s YouTube video course — Lecture Notes. Part 1.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qqsBbdQRCiCDw8Kr5GKu9w.jpeg" /><figcaption>Photo by JJ Ying — <a href="https://unsplash.com/photos/8bghKxNU1j0">https://unsplash.com/photos/8bghKxNU1j0</a></figcaption></figure><p>This post is based entirely on Andrej Karpathy’s video tutorial “The spelled-out intro to neural networks” — <a href="https://www.youtube.com/watch?v=VMj-3S1tku0&amp;t=4822s">https://www.youtube.com/watch?v=VMj-3S1tku0&amp;t=4822s</a></p><p>In this post, we will learn about the very basic mechanisms of neural networks — how does a network learn to classify dog and cat images? How does a network learn to generate realistic text? The basics behind all of these tasks are the same!</p><p>Let’s start by asking ourselves some questions about neural networks (and answering them):</p><ol><li>What are neural networks? They are simply mathematical functions that have an input and an output.</li><li>What does it mean to “train” a neural network? It means tuning the function’s parameters such that its output is as close as possible to the truth.</li><li>How do we tune these parameters? Using an algorithm called backpropagation, which calculates the derivative of the target term which we aim to minimize, with respect to each parameter of the network, and uses these derivatives to nudge each parameter in the right direction (that will lead to minimization of the target term).</li></ol><p>In essence, after you read the above questions, you know how neural networks work. But of course, we wish to go (a bit) deeper than that. Hence, we will follow Andrej Karpthy’s excellent course on YouTube, and dive into the nitty-gritty of neural networks, piece by piece.</p><p>This post will be organized as follows:</p><ul><li>Part 1. What is a Derivative?</li><li>Part 2. Micrograd — A Minimalistic Autodiff Tool</li><li>Part 3. Micrograd — Manual Calculation of Gradients</li><li>Part 4. Micrograd — Building a simple neuron</li><li>Part 5. Manual Backpropagation through a Neuron</li><li>Part 6. Implementing Backward Operations</li><li>Part 7. Adding support for constants</li><li>Part 8. Breaking up Tanh into its constituting parts</li><li>Part 9. Comparing Micrograd to Pytorch</li><li>Part 10. Building a multi-layer perceptron (MLP)</li><li>Part 11. Training the MLP</li><li>Part 12. Summary</li></ul><p>Just like Andrej, we will start at the very (very) basics. What is a Derivative? It might seem basic but it’s important to get a very firm grasp on this subject before moving on. Derivatives are the core of the backpropagation algorithm, which is the fuel behind (almost) every modern neural network.</p><h3>Part 1. What is a Derivative?</h3><p>The layman’s definition of a derivative is this:</p><p>“I have a function in which y is dependent on the value of x. If I nudge x just a tiny bit at a certain point, what would be the rate of change of y in response to that?”</p><p>For those of you that prefer equations, let’s see what Wikipedia has to say about this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/179/0*plAEMjGAHrcssw3t" /><figcaption>Eq. 1 — What is a derivative? Taken from Wikipedia.</figcaption></figure><p>As we see in eq.1 — We take f(a) and nudge it by a tiny amount — h. We take the difference between f(a) and the nudged version — f(a + h). Then we divide it by the amount that we nudged — h, to get the rate of change. Also, notice the lim in the equation. This means that the true slope will be obtained when the nudging amount h is very close to 0.</p><p>Let’s see some Python code to make this clearer. We will first create a polynomial function and plot it using matplotlib.</p><pre>def f(x):<br>    return 3 * x ** 2 - 4 * x + 5<br><br>xs = np.arange(-5, 5, 0.25)<br>ys = f(xs)<br>plt.plot(xs, ys)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/372/1*WQJQBCXwlrPVIlt45mV-Qg.png" /><figcaption>Fig. 1 — f(x) plotted.</figcaption></figure><p>What is the slope at x=3? Before reading ask yourself — is positive? negative?</p><p>Let’s calculate it using the definition of the derivative we just saw.</p><pre>h = 0.00001<br>x = 3<br>(f(x + h) - f(x)) / h</pre><p>Out: 14.00003000000538</p><p>And indeed, we get the correct answer (you can validate this by using calculus to get the derivative and placing 3 instead of x).</p><p>Let us now do the same thing with a slightly more complex function that depends on several parameters. We will check what is the derivative of the function with respect to each one of its parameters.</p><pre>def g(a, b, c):<br>    return a * b + c<br><br>a = 2<br>b = -3<br>c = 10</pre><p>Before reading on, try to think what will be the slope of the function g(a, b, c) with respect to each of its parameters.</p><pre>print(f&#39;Slope w.r.t a: {(g(a + h, b, c) - g(a, b, c)) / h}&#39;)<br>print(f&#39;Slope w.r.t b: {(g(a, b + h, c) - g(a, b, c)) / h}&#39;)<br>print(f&#39;Slope w.r.t c: {(g(a, b, c + h) - g(a, b, c)) / h}&#39;)</pre><p>Out:</p><pre>Slope w.r.t a: -3.000000000064062<br>Slope w.r.t b: 2.0000000000131024<br>Slope w.r.t c: 0.9999999999621422</pre><p>We see that if we nudge a by a tiny amount the slope is negative. This makes sense because a is multiplying b which is a negative number, which means we get more of this negativeness in the function. The logic is similar for the other parameters :)</p><h3>Part 2. Micrograd — A Minimalistic Autodiff Tool</h3><p>In this tutorial, we are going to build Micrograd — a minimalistic auto-differentiation utility in Python. Autodiff is the basis behind modern neural network libraries such as TensorFlow and Pytorch and is an algorithm for automatically calculating gradients in computational graphs (such as neural networks). The tendency is to think that Autodiff is a very complicated piece of machinery, yet Andrej shows in his course that we can create a fully functional Autodiff tool (Micrograd) in just 100~ lines of code.</p><p>The full version of Micrograd (by Andrej Karpathy) can be found here — <a href="https://github.com/karpathy/micrograd">https://github.com/karpathy/micrograd</a>.</p><p>The purpose of an Autodiff tool is to be able to produce the slope of any function with respect to any one of its parameters. This is useful in neural network training because neural networks are giant functions with many parameters, and to improve them we need to calculate their slope with respect to our target, to understand how we should nudge each of the function’s parameters.</p><p>An Autodiff tool works by keeping track of a computational graph, which it uses to understand which parameters affected which parts of the function calculation and how. As we will see, each type of mathematical operator has a different derivative, and by chaining together these derivatives using the chain rule we will be able to obtain the slope of a very deep set of operations.</p><p>Let’s dive in. First, we need some way to represent a number in Python which will allow us to follow the calculations that it is involved in. This will help us build the computational graph needed for Autodiff.</p><pre>class Value:<br>    def __init__(self, data):<br>        self.data = data<br><br>    def __repr__(self):<br>        return f&#39;Value(data={self.data})&#39;<br><br>a = Value(2.0)<br>b = Value(-3.0)<br>print(a)<br>print(b)</pre><p>Out:</p><pre>Value(data=2.0)<br>Value(data=-3.0)</pre><p>We created a class called Value which keeps track of a value of a number, and implements the __repr__ function, which tells Python how to print the representation of this object. Not very useful right? What if we want to add two numbers, for example? Let’s add some computational abilities to this class.</p><pre>class Value:<br>    def __init__(self, data):<br>        self.data = data<br><br>    def __repr__(self):<br>        return f&#39;Value(data={self.data})&#39;<br><br>    def __add__(self, other):<br>        return Value(self.data + other.data)<br><br>    def __mul__(self, other):<br>        return Value(self.data * other.data)<br><br>a = Value(2.0)<br>b = Value(-3.0)<br>c = Value(10.0)<br>a * b + c  # equivalent to (a.__mul__(b)).__add__(c)</pre><p>Out:</p><p>Value(data=4.0)</p><p>Great! We are now able to reproduce our function from before using the Value class. Notice that a * b + c is equivalent to (a.__mul__(b)).__add__(c) and this is because Python knows it should use the __add__ and __mul__ functions (if they were implemented) when using + and * operators. But wait, this still doesn’t give us the computational graph we wanted so we can calculate derivatives. Let’s make some changes to the class to keep track of the calculations that occur.</p><pre>class Value:<br>    def __init__(self, data, prev=(), op=&#39;&#39;, label=&#39;&#39;):<br>        self.data = data<br>        self._prev = set(prev)<br>        self._op = op<br>        self.label = label<br>        self.grad = 0<br><br>    def __repr__(self):<br>        return f&#39;Value(data={self.data})&#39;<br><br>    def __add__(self, other):<br>        return Value(self.data + other.data, prev=(self, other), op=&#39;+&#39;)<br><br>    def __mul__(self, other):<br>        return Value(self.data * other.data, prev=(self, other), op=&#39;*&#39;)<br><br>a = Value(2.0, label=&#39;a&#39;)<br>b = Value(-3.0, label=&#39;b&#39;)<br>c = Value(10.0, label=&#39;c&#39;)<br>ans = a * b + c  # equivalent to (a.__mul__(b)).__add__(c)<br>print(f&#39;ans._prev: {ans._prev}&#39;)<br>print(f&#39;ans._op: {ans._op}&#39;)</pre><p>Out:</p><pre>ans._prev: {Value(data=10.0), Value(data=-6.0)}<br>ans._op: +</pre><p>We added new parameters to the class: self._prev and self._op . These parameters are used to keep track of the participating elements in each calculation, and which operator was used in each calculation. We can see that we printed the _prev and _op values of the final value of the function, of which the last operation was an addition, in which c and a*b participated (if we were to print the _prev and _op values of the inner element, we should see 2, -3, and *). We also added a label parameter that will help us visualize the computational graph soon and a grad parameter that will hold derivative information for each parameter.</p><p>We will now add a function that will help us visualize the computational graph which was built by the Value objects.</p><pre>from graphviz import Digraph<br><br>def trace(root):<br>    nodes, edges = set(), set()<br>    def build(v):<br>        if v not in nodes:<br>            nodes.add(v)<br>            for child in v._prev:<br>                edges.add((child, v))<br>                build(child)<br>    build(root)<br>    return nodes, edges<br><br>def draw_dot(root):<br>    dot = Digraph(format=&#39;svg&#39;, graph_attr={&#39;rankdir&#39;: &#39;LR&#39;})<br>    nodes, edges = trace(root)<br>    for n in nodes:<br>        uid = str(id(n))<br>        dot.node(name=uid, label=f&#39;{n.label} | data: {n.data:.4f} | grad: {n.grad:.4f}&#39;, shape=&#39;record&#39;)<br>        if n._op:<br>            dot.node(name=uid + n._op, label=n._op)<br>            dot.edge(uid + n._op, uid)<br>    for n1, n2 in edges:<br>        dot.edge(str(id(n1)), str(id(n2)) + n2._op)<br>    return dot<br><br>a = Value(2.0, label=&#39;a&#39;)<br>b = Value(-3.0, label=&#39;b&#39;)<br>c = Value(10.0, label=&#39;c&#39;)<br>e = a * b; e.label = &#39;e&#39;<br>d = e + c; d.label = &#39;d&#39;<br>f = Value(-2.0, label = &#39;f&#39;)<br>L = d * f; L.label = &#39;L&#39;<br><br>draw_dot(L)</pre><p>Out:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/987/1*Y4Coo00XzorX6FIrJbpI5w.png" /><figcaption>output of draw_dot — simple computational graph</figcaption></figure><p>We created a draw_dot function that takes a Value object and builds the computational graph associated with it. First, it recursively finds all nodes and edges using the trace function and then continues to draw these elements using the graphviz library. Notice that we explicitly had to label the e , d and L nodes for their names to appear in the visualization.</p><p>We will now recap what we did so far. We created a Value class that allows us to create a simple computational graph built of addition and multiplication operators. We are also able to visualize this graph using the graphviz library. But we already saw before that in order to learn neural network parameters, we need to be able to calculate the gradients for each parameter in the computational graph with respect to the output node. As a first step, we will calculate each of the gradients manually and populate the respective gradient parameter.</p><h3>Part 3. Micrograd — Manual Calculation of Gradients</h3><p>Our function consists of seven parameters — a, b, c, d, e, f, L. Let us think of the gradients for each, with respect to the output — L.</p><ul><li>The gradient of Lwith respect to itself is simply 1. Why? How much will Lchange when we add h to it? Well of course, by h. Since the gradient is the relative change, the answer will be h/h = 1. That was easy, right?</li><li>Now, what is the gradient of d with respect to L? We have that L=d*f. From calculus, we know that dL/dd = f. Let’s also prove this using the definition of a derivative. As a reminder, the definition is (f(x+h) — f(x))/h (rise over run). So if we want the derivative of L with respect to d we get ((d+h) * f-d * f)/h, which is (df + hf — df)/h, which is hf/h, which is f = -2.</li><li>Similarly, dL/df = d = 4.</li><li>Now we are getting to the tricky part. What is dL/dc? What is the ratio of change of L when we slightly change c? It is trickier because the effect isn’t direct, but rather passes through the variable d. Luckily for us, calculus has us covered with the chain rule. What does it entail? That if we want to calculate dL/dc which passes through d, we simply need to calculate dL/dd and dd/dc and multiply between them.<br>Easy!<br>So, we already know dL/dd. As for dd/dc, d=c+e, how much does d change when we add h to c? By h of course. Since a derivative is the ratio of change, it will be h/h=1. This means that according to the chain rule, we get dL/dc = dL/dd*dd/dc = f*1 = f = -2.</li><li>By symmetry we get dL/de = f*1 = -2</li><li>What about dL/da. From the chain rule, we get that it is dL/de * de/da. We already know dL/da. de/da for e=a*b is just b as we saw previously. This means that dL/da = dL/de*de/da = f * b = -2 * -3 = 6.</li><li>By symmetry we get dL/db = dL/de * de/db = f * a = -2 * 2 = -4</li></ul><p>In the video course, Andrej proves all of the gradients are correct using the definition of the gradient, as we did previously (checking rise over run). For brevity, we will skip this in this article.</p><p>Finally, let’s manually assign all the gradients we found to their respective parameters and view the computational graph again.</p><pre>a.grad = 6<br>b.grad = -4<br>c.grad = -2<br>d.grad = 4<br>e.grad = -2<br>f.grad = 4<br>L.grad = 1<br>draw_dot(L)</pre><p>Out:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/989/1*0Slwg-D05vN7uFyWlnSawg.png" /><figcaption>output of draw_dot — added gradients</figcaption></figure><p>Let’s now imagine that the function above is a neural network that we want to optimize. Our optimization goal will be to increase the value of L. For each parameter, we calculated a gradient which tells us which direction should this parameter move in order to increase the value of L. We will use this information to optimize the function (note that we will do this only for the nodes which we have control over — that are not the direct result of an operation):</p><pre>a.data += a.grad * 0.01<br>b.data += b.grad * 0.01<br>c.data += c.grad * 0.01<br>f.data += f.grad * 0.01<br><br>e = a * b<br>d = e + c<br>L = d * f<br><br>print(L.data)</pre><p>out: -7.286496</p><p>Indeed we see that after we optimized our parameters using the gradients we manually calculated, and ran the forward pass of our network again, we managed to increase the value of L.</p><h3>Part 4. Micrograd — Building a simple neuron</h3><p>We are now ready to move on to real neural networks. But first — what is an artificial neuron?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/551/1*TCGG2b3WLLI6qg9h-_PP7Q.png" /><figcaption>Fig. 1 — An artificial neuron. Image by the author.</figcaption></figure><p>In Figure 1 we see a depiction of an artificial neuron that contains 3 inputs — X1, X2, and X3. Each input is multiplied by its corresponding weight and these multiplications are added up. An additional bias term b is added as well. This bias term is inherent to the neuron and will be the same for every input. Finally, an activation function is applied. In this tutorial, we will use the <em>Tanh</em> function, which squashes the input between -1 and 1. The artificial neuron vaguely mimics what we think happens in a real neuron, where neurons receive input from other neurons via synapses and send their outputs via axons.</p><p>Now, let us implement an artificial neuron and understand how to use backpropagation to tune its weights in order to maximize a certain target function.</p><pre>x1 = Value(2.0, label=&#39;x1&#39;)<br>x2 = Value(0.0, label=&#39;x2&#39;)<br>w1 = Value(-3.0, label=&#39;w1&#39;)<br>w2 = Value(1.0, label=&#39;w2&#39;)<br>b = Value(6.7, label=&#39;b&#39;)<br><br>x1w1 = x1*w1; x1w1.label = &#39;x1*w1&#39;<br>x2w2 = x2*w2; x2w2.label = &#39;x2*w2&#39;<br>x1w1x2w2 = x1w1 + x2w2; x1w1x2w2.label = &#39;x1*w1 + x2*w2&#39;<br>n = x1w1x2w2 + b; n.label = &#39;n&#39;<br># o = n.tanh()<br>draw_dot(n)</pre><p>Out:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7EpxVCh_nO5QWT18RJk5Jg.png" /><figcaption>output of draw_dot — a neuron</figcaption></figure><p>We implemented a simple neuron with two inputs: x1 and x2. Notice that we commented out the use of the <em>Tanh</em> activation function. Why? Because we haven’t yet implemented it in our Value class! So let’s do that. But first — what is exactly a <em>Tanh</em> function?</p><p>This is the definition of <em>Tanh</em> taken from Wikipedia:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/325/0*uqUu6w2OE41fv7dp" /><figcaption>Eq. 2 — Tanh (from <a href="https://en.wikipedia.org/wiki/Hyperbolic_functions">Wikipedia</a>)</figcaption></figure><p>We have two options now — one will be to implement the exp function in Value. This will allow us to create the <em>Tanh</em> function out of its basic building blocks of addition (subtraction), multiplication (division), and exponentiation. The second option will be to implement the <em>Tanh</em> function directly. In backpropagation, we don’t have to explicitly implement the smallest parts of the computational graph — we can use arbitrarily large building blocks as long as we can implement their forward pass and derivative. We will choose the second option and implement <em>Tanh</em> in the Value class.</p><pre>class Value:<br>    def __init__(self, data, prev=(), op=&#39;&#39;, label=&#39;&#39;):<br>        self.data = data<br>        self._prev = set(prev)<br>        self._op = op<br>        self.label = label<br>        self.grad = 0<br><br>    def __repr__(self):<br>        return f&#39;Value(data={self.data})&#39;<br><br>    def __add__(self, other):<br>        return Value(self.data + other.data, prev=(self, other), op=&#39;+&#39;)<br><br>    def __mul__(self, other):<br>        return Value(self.data * other.data, prev=(self, other), op=&#39;*&#39;)<br><br>    def tanh(self):<br>        x = self.data<br>        t = (math.exp(2*x) - 1)/(math.exp(2*x) + 1)<br>        out = Value(t, (self,), &#39;tanh&#39;)<br>        return out</pre><p>We added the tanh function, which contains the calculation for the <em>Tanh</em> operation as we saw earlier. Notice that we pass a 1-item tuple as the prev parameter because <em>Tanh</em> operates only on one input.</p><p>Now, after we comment out the <em>Tanh</em> operation we can plot the full computational graph:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kgtiuMraoW067LQ6mqnlRw.png" /><figcaption>output of draw_dot — neuron with Tanh</figcaption></figure><p>Now it’s time to backpropagate through our neuron!</p><h3>Part 5. Manual Backpropagation through a Neuron</h3><p>I think this part is a nice exercise to do on your own, so if you are up to it — stop here and work the grad values of each of the parameters with respect to o(you don’t have to :)).</p><p>Let’s work through it:</p><ul><li>The gradient of o with respect to itself is 1.</li><li>The gradient of owith respect to n is the gradient of the <em>Tanh </em>function, which is defined by: dtanh(x)/dx = 1 — tanh(x)^2. We will also multiply this by the gradient of o according to the chain rule. So we get that n.grad = (1 — o.data**2) * o.grad = 0.5.</li><li>The gradient of o with respect to b according to the chain rule is dn/db * do/dn. Remember that the addition operation simply passes the gradient from the previous step because the local derivative is 1. So we get that b.grad = 1*n.grad = 0.5.</li><li>Similarly, x1w2x2w2.grad = 1*n.grad = 0.5.</li><li>How about x1w1.grad? It’s also a result of an addition operation, hence: x1w1.grad = 1*x1w1x2w2.grad = 0.5.</li><li>Similarly, x2w2.grad = 1 * x1w1x2w2.grad = 0.5.</li><li>For x1.grad we have a multiplication operation, of which the local gradient is the other parameter. To this, we apply the chain rule, hence x1.grad = x1w1.grad * w1 = -3 * 0.5 = -1.5.</li><li>Similarly, we get that w1.grad = x1w1.grad * x1 = 2 * 0.5 = 1, and x2.grad = x2w2.grad * w2 = 0.5 * 1 = 0.5, and w2.grad = x2w2.grad * x2 = 0.5 * 0 = 0.</li></ul><p>Let’s sum it up in code and plot the resulting graph:</p><pre>o.grad = 1  # do/do<br>n.grad = (1 - o.data ** 2) * o.grad  # do/dn * do/do<br>b.grad =  1 * n.grad # dn/db * do/dn<br>x1w1x2w2.grad = 1 * n.grad  # dn/dx1w1x2w2 * do/dn<br>x1w1.grad =  1 * x1w1x2w2.grad # dx1w1x2w2/dx1w1 * do/dx1w1x2w2<br>x2w2.grad =  1 * x1w1x2w2.grad # dx1w1x2w2/dx1w1 * do/dx1w1x2w2<br>w1.grad = x1.data * x1w1.grad  #dx1w1/dw1 * do/dx1w1<br>x1.grad = w1.data * x1w1.grad  #dx1w1/dx1 * do/dx1w1<br>w2.grad = x2.data * x2w2.grad  #dx2w2/dw2 * do/dx2w2<br>x2.grad = w2.data * x2w2.grad  #dx2w2/dx2 * do/dx2w2<br>draw_dot(o)</pre><p>Out:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fmaDSqeNLPmQh4fUIaUFnA.png" /><figcaption>output of draw_dot — neuron with gradients</figcaption></figure><p>Great! We know how to do backpropagation! So is this how they do it in GPT-3 (which has 170B parameters)? Do they write 170B lines of code to set the gradient of each parameter? Well, no. Let’s see how to do it properly and automatically.</p><h3>Part 6. Implementing Backward Operations</h3><p>We are now going to add a _backward function for each operation in our Value class. This function will calculate the gradient for each computational node instead of us having to do it manually.</p><pre>class Value:<br>    def __init__(self, data, prev=(), op=&#39;&#39;, label=&#39;&#39;):<br>        self.data = data<br>        self._prev = set(prev)<br>        self._op = op<br>        self.label = label<br>        self.grad = 0<br>        self._backward = lambda: None<br><br>    def __repr__(self):<br>        return f&#39;Value(data={self.data})&#39;<br><br>    def __add__(self, other):<br>        out = Value(self.data + other.data, prev=(self, other), op=&#39;+&#39;)<br>        def _backward():<br>            self.grad = 1.0 * out.grad<br>            other.grad = 1.0 * out.grad<br>        out._backward = _backward<br>        return out<br><br>    def __mul__(self, other):<br>        out = Value(self.data * other.data, prev=(self, other), op=&#39;*&#39;)<br>        def _backward():<br>            self.grad = other.data * out.grad<br>            other.grad = self.data * out.grad<br>        out._backward = _backward<br>        return out<br><br>    def tanh(self):<br>        x = self.data<br>        t = (math.exp(2*x) - 1)/(math.exp(2*x) + 1)<br>        out = Value(t, (self,), &#39;tanh&#39;)<br>        def _backward():<br>            self.grad = (1.0 - out.data ** 2) * out.grad<br>        out._backward = _backward<br>        return out</pre><p>Let’s go over the code.</p><ul><li>The default _backward function returns nothing. This will be the case for leaf nodes which are not part of any operation.</li><li>In addition, the _backward function sets the gradient of both operands according to the chain rule, which is the local derivative (1) times the previous derivative in the chain (the derivative of out). Note that we are assuming that the derivative of out has already been calculated.</li><li>The case for multiplication is similar, except that the local gradient is the other operand instead of 1 — other.grad for self, and self.grad for other.</li><li>For the <em>Tanh </em>operation, we don’t have an other so we only need to set self.grad. Just like before, we use the local derivative of <em>Tanh</em> and multiply it with the derivative of out which we assume has been calculated previously.</li></ul><p>Let’s use the _backward functions:</p><pre>o.grad = 1.0<br>o._backward()<br>n._backward()<br>b._backward()<br>x1w1x2w2._backward()<br>x2w2._backward()<br>x1w1._backward()<br>draw_dot(o)</pre><p>Out:</p><p><em>I will avoid pasting the graph, as it is identical to the previous case (only this time we used the </em><em>_backward functions in the correct order to calculate the gradients).</em></p><p>Note that we had to set o.grad = 1.0 manually. Why? Because the placeholder gradient value for each node is 0. The backward function always depends on the previous gradient in the computational graph and if it was 0 then all gradients will turn out to be 0 (think about it).</p><p>But do we have to call the _backward function manually? Of course not! We can use a loop here, but notice that we need to iterate over the nodes in a certain order. We can’t call _backward on a node that has neighbors connected via outgoing edges whose gradients haven’t been calculated already. For this, we need something called topological sorting.</p><p>Let’s understand what is topological sorting using an example. Take this graph:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/305/0*Fbj54jlFcER3IcSI.png" /><figcaption>Fig 2. Topological sorting (from <a href="https://en.wikipedia.org/wiki/Topological_sorting">Wikipedia</a>)</figcaption></figure><p>Here are three options to topologically sort this graph (taken from Wikipedia — <a href="https://en.wikipedia.org/wiki/Topological_sorting">https://en.wikipedia.org/wiki/Topological_sorting</a>):</p><ul><li>5, 7, 3, 11, 8, 2, 9, 10 (visual top-to-bottom, left-to-right)</li><li>3, 5, 7, 8, 11, 2, 9, 10 (smallest-numbered available vertex first)</li><li>5, 7, 3, 8, 11, 10, 9, 2 (fewest edges first)</li></ul><p>Notice that in all sorting options, for each node <em>u </em>that has an outgoing edge to node <em>v</em>, node <em>v</em> will be after <em>u</em> in the topological sorting.</p><p>Let’s implement a simple form of topological sorting and use it to apply the _backward functions in our computational graph:</p><pre>topo = []<br>visited = set()<br><br>def build_topo(v, topo, visited):<br>    if v not in visited:<br>        visited.add(v)<br>        for child in v._prev:<br>            build_topo(child, topo, visited)<br>        topo.append(v)</pre><p>The above is a simple implementation of topological sorting. Using recursion we make sure that all of the offspring of a certain node will be added to the list topo before the said node. Instead of calling this function explicitly, we will make the code a bit cleaner and make it part of our Value class, such that only a single function must be called to calculate gradients for the whole computational graph.</p><pre>class Value:<br>    def __init__(self, data, prev=(), op=&#39;&#39;, label=&#39;&#39;):<br>        self.data = data<br>        self._prev = set(prev)<br>        self._op = op<br>        self.label = label<br>        self.grad = 0<br>        self._backward = lambda: None<br><br>    def __repr__(self):<br>        return f&#39;Value(data={self.data})&#39;<br><br>    def __add__(self, other):<br>        out = Value(self.data + other.data, prev=(self, other), op=&#39;+&#39;)<br>        def _backward():<br>            self.grad = 1.0 * out.grad<br>            other.grad = 1.0 * out.grad<br>        out._backward = _backward<br>        return out<br><br>    def __mul__(self, other):<br>        out = Value(self.data * other.data, prev=(self, other), op=&#39;*&#39;)<br>        def _backward():<br>            self.grad = other.data * out.grad<br>            other.grad = self.data * out.grad<br>        out._backward = _backward<br>        return out<br><br>    def tanh(self):<br>        x = self.data<br>        t = (math.exp(2*x) - 1)/(math.exp(2*x) + 1)<br>        out = Value(t, (self,), &#39;tanh&#39;)<br>        def _backward():<br>            self.grad = (1.0 - out.data ** 2) * out.grad<br>        out._backward = _backward<br>        return out<br><br>    def backward(self):<br>        topo = []<br>        visited = set()<br>        def build_topo(v):<br>            if v not in visited:<br>                visited.add(v)<br>                for child in v._prev:<br>                    build_topo(child)<br>                topo.append(v)<br>        build_topo(self)<br>        self.grad = 1<br>        for node in reversed(topo):<br>            node._backward()</pre><p>Now, all we need to do to calculate gradients is:</p><pre>o.backward()<br>draw_dot(o)</pre><p>Out:</p><p><em>Same graph as before</em></p><p>Now, I want to show you a slight problem we have in our current implementation of Value — what happens when a node in the computational graph gets used more than once?</p><h3>Part 6. What happens when a node gets used more than once?</h3><p>Take this example:</p><pre>a = Value(-2.0, label=&#39;a&#39;)<br>b = Value(3.0, label=&#39;b&#39;)<br>d = a * b; d.label = &#39;d&#39;<br>e = a + b; e.label = &#39;e&#39;<br>f = d * e; f.label = &#39;f&#39;<br><br>f.backward()<br>draw_dot(f)</pre><p>Out:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/793/1*W6k7EaH-DAB61SFLTnXJxw.png" /><figcaption>output of draw_dot — using a component twice in the computational graph</figcaption></figure><p>What should be the gradients of a and b in this case? According to the multiplication node — the gradient of a should be 3 and the gradient of b should be -2 (in the multiplication of two numbers the local gradient is the other operand). According to the addition node, the gradient of both should be -6. We see that in this case the calculation was based on the addition node, but that is just because of the arbitrary topological sorting.</p><p>Why does this happen? Let’s observe the _backward function of the __add__ operation:</p><pre>def _backward():<br>    self.grad = 1.0 * out.grad<br>    other.grad = 1.0 * out.grad</pre><p>We see that we set the gradients of self and other, while ignoring the previous values that might have been already calculated for it. In backpropagation, the gradient of a certain node will be calculated additively — the effects of all calculations will add up.</p><p>We can fix this situation quite simply by changing the = operator in the _backward functions to += s.t the effects will add up. Like so:</p><pre>def _backward():<br>    self.grad += 1.0 * out.grad<br>    other.grad += 1.0 * out.grad</pre><p>After we fixed our _backward functions and run the previous code again, we get this computational graph:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/792/1*ohDi8D0vO7ht57fvgm7QMA.png" /><figcaption>output of draw_dot — using a component twice in the computational graph, fixed gradients</figcaption></figure><p>We see here the correct gradients for a and b where the effects of the + and * nodes add up, instead of overriding each other.</p><h3>Part 7. Adding support for constants</h3><p>What will happen if we run this code?</p><pre>a = Value(2.0)<br>a + 2</pre><p>We get this error — AttributeError: ‘int’ object has no attribute ‘data’. Why? Because the __add__ function of Value is expecting another Value object. We can overcome this quite simply by performing this fix for the __add__ function (and similarly for the __mul__ function):</p><pre>def __add__(self, other):<br>    other = other if isinstance(other, Value) else Value(other)<br>    out = Value(self.data + other.data, prev=(self, other), op=&#39;+&#39;)<br>    def _backward():<br>        self.grad += 1.0 * out.grad<br>        other.grad += 1.0 * out.grad<br>    out._backward = _backward<br>    return out</pre><p>We simply check whether other is of type Value. If not, we convert it :)</p><p>Now what happens when we run this code?</p><pre>a = Value(2.0)<br>2 + a</pre><p>Seems like it should work right? Turns out it won’t (we get this error — TypeError: unsupported operand type(s) for +: ‘int’ and ‘Value’). Python in this case is trying to run 2.__add__(a). The default __add__ function of Python obviously doesn’t know how to handle Value objects, because we made them up. How do we fix this?</p><p>For this purpose, Python supports the __radd__ function:</p><pre>def __radd__(self, other):<br>    return self + other</pre><p>The way it works is that if Python fails to call __add__ for some reason and __radd__ is available, it will swap the operands of __add__ and call __radd__. Thus, a + 2 will be called instead, and we already saw that it works. We will act accordingly with __mul__ and __rmul__, of course.</p><h3>Part 8. Breaking up Tanh into its constituting parts</h3><p>Currently, our Value class supports the <em>Tanh </em>operation as a complete block. What if we wanted to break <em>Tanh</em> into its constituting parts by explicitly including the <em>Tanh</em> calculation in a computational graph?</p><p>To achieve this we need to add several operators. Let’s remind ourselves of the <em>Tanh</em> implementation:</p><pre>def tanh(self):<br>    x = self.data<br>    t = (math.exp(2*x) - 1)/(math.exp(2*x) + 1)<br>    out = Value(t, (self,), &#39;tanh&#39;)<br>    return out</pre><p>To break up this operation we need the following operators — exponentiation (exp), power (pow), division, and subtraction.</p><p>For division, we will implement the following:</p><pre>def __truediv__(self, other):<br>    return self * other ** -1</pre><p>The operation we just used is mathematically equivalent to self/other, but notice that we depend on the power (**) operator, which we haven’t yet implemented. Let’s do that:</p><pre>def __pow__(self, other):<br>    assert isinstance(other, (int, float)), &quot;only supporting pow for int/float&quot;<br>    out = Value(self.data ** other, (self,), f&#39;**{other}&#39;)<br>    def _backward():<br>        self.grad += other * (self.data ** (other - 1)) * out.grad<br>    out._backward = _backward<br>    return out</pre><p>Let’s note several things about __pow__. First, we only support int or float inputs. This means we don’t support power with more complex inputs (namely arbitrary Value objects) because that would lead to different derivative rules. Second, we implement the _backward function using the calculus rule where dxⁿ/dx=nxⁿ⁻¹.</p><p>Finally, we will need to support subtraction, and we will simply utilize addition for that as so:</p><pre>def __sub__(self, other):<br>    return self + (-other)</pre><p>For this we need to support negation, so we add:</p><pre>def __neg__(self):<br>    return self * -1</pre><p>Let’s view our wholeValue class so far:</p><pre>class Value:<br>    def __init__(self, data, prev=(), op=&#39;&#39;, label=&#39;&#39;):<br>        self.data = data<br>        self._prev = set(prev)<br>        self._op = op<br>        self.label = label<br>        self.grad = 0<br>        self._backward = lambda: None<br><br>    def __repr__(self):<br>        return f&#39;Value(data={self.data})&#39;<br><br>    def __add__(self, other):<br>        other = other if isinstance(other, Value) else Value(other)<br>        out = Value(self.data + other.data, prev=(self, other), op=&#39;+&#39;)<br>        def _backward():<br>            self.grad += 1.0 * out.grad<br>            other.grad += 1.0 * out.grad<br>        out._backward = _backward<br>        return out<br><br>    def __radd__(self, other):<br>        return self + other<br><br>    def __neg__(self):<br>        return self * -1<br><br>    def __sub__(self, other):<br>        return self + (-other)<br><br>    def __mul__(self, other):<br>        other = other if isinstance(other, Value) else Value(other)<br>        out = Value(self.data * other.data, prev=(self, other), op=&#39;*&#39;)<br>        def _backward():<br>            self.grad += other.data * out.grad<br>            other.grad += self.data * out.grad<br>        out._backward = _backward<br>        return out<br><br>    def __rmul__(self, other):<br>        return self * other<br><br>    def tanh(self):<br>        x = self.data<br>        t = (math.exp(2*x) - 1)/(math.exp(2*x) + 1)<br>        out = Value(t, (self,), &#39;tanh&#39;)<br>        def _backward():<br>            self.grad += (1.0 - out.data ** 2) * out.grad<br>        out._backward = _backward<br>        return out<br><br>    def exp(self):<br>        x = self.data<br>        t = math.exp(x)<br>        out = Value(t, (self,), &#39;exp&#39;)<br>        def _backward():<br>            self.grad += t * out.grad<br>        out._backward = _backward<br>        return out<br><br>    def __truediv__(self, other):<br>        return self * other ** -1<br>    <br>    def __pow__(self, other):<br>        assert isinstance(other, (int, float)), &quot;only supporting pow for int/float&quot;<br>        out = Value(self.data ** other, (self,), f&#39;**{other}&#39;)<br>        def _backward():<br>            self.grad += other * (self.data ** (other - 1)) * out.grad<br>        out._backward = _backward<br>        return out<br><br>    def backward(self):<br>        topo = []<br>        visited = set()<br>        def build_topo(v):<br>            if v not in visited:<br>                visited.add(v)<br>                for child in v._prev:<br>                    build_topo(child)<br>                topo.append(v)<br>        build_topo(self)<br>        self.grad = 1<br>        for node in reversed(topo):<br>            node._backward()</pre><p>Now, we shall break up the <em>Tanh</em> function into its constituent parts, such that the whole calculation will appear in the computational graph:</p><pre>x1 = Value(2.0, label=&#39;x1&#39;)<br>x2 = Value(0.0, label=&#39;x2&#39;)<br>w1 = Value(-3.0, label=&#39;w1&#39;)<br>w2 = Value(1.0, label=&#39;w2&#39;)<br>b = Value(6.8813735870195432, label=&#39;b&#39;)<br><br>x1w1 = x1*w1; x1w1.label = &#39;x1*w1&#39;<br>x2w2 = x2*w2; x2w2.label = &#39;x2*w2&#39;<br>x1w1x2w2 = x1w1 + x2w2; x1w1x2w2.label = &#39;x1*w1 + x2*w2&#39;<br>n = x1w1x2w2 + b; n.label = &#39;n&#39;<br><br>e = (2*n).exp()<br>o = (e - 1) / (e + 1)<br><br>o.backward()<br>draw_dot(o)</pre><p>Out:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LUi2cPrZSQRlXGA2K4v1RA.png" /><figcaption>output of draw_dot — breaking up Tanh</figcaption></figure><p>Great! We now have a much larger computational graph and can see all the components of the <em>Tanh</em> function. Notice that the gradients for <em>w1</em> and <em>w2</em> are the same as before, thus we know that this graph is operationally identical to the previous (where the <em>Tanh</em> operation was implicit).</p><p>What do we learn from this exercise? In backpropagation and neural network training, we can implement an operation as abstract as we want. As long as we can define the forward and backward pass by calculating the local gradient for such an operation it doesn’t matter that it by itself is composed of other operations (think of the fact that multiplication can be described by many additions).</p><h3>Part 9. Comparing Micrograd to Pytorch</h3><p>Up until now, we developed our own auto-differentiable computation graph class — Value. We validated that it is working as expected and that we can back-propagate through arbitrarily complex graphs as long as they are made of supported building blocks.</p><p>Despite this, our implementation is very naive computation-wise and there are better and faster libraries out there that do just this. One example is the popular Pytorch library, developed by Meta. The Pytorch library allows the creation of highly efficient computational graphs which can be parallelized and optimized for different hardware including CPUs, GPUs, and TPUs. It is used extensively in industry and cutting-edge research — mostly for neural networks.</p><p>To validate our implementation of Micrograd we will implement the exact same computational graph from before in Pytorch and confirm that the results are identical.</p><pre>import torch<br>x1 = torch.Tensor([2.0]).double(); x1.requires_grad = True<br>x2 = torch.Tensor([0.0]).double(); x2.requires_grad = True<br>w1 = torch.Tensor([-3.0]).double(); w1.requires_grad = True<br>w2 = torch.Tensor([1.0]).double(); w2.requires_grad = True<br>b = torch.Tensor([6.8813735870195432]).double(); b.requires_grad = True<br><br>n = x1*w1 + x2*w2 + b<br>o = torch.tanh(n)<br><br>print(o.data.item())<br>o.backward()<br><br>print(&#39;----&#39;)<br>print(&#39;x2&#39;, x2.grad.item())<br>print(&#39;w2&#39;, w2.grad.item())<br>print(&#39;x1&#39;, x1.grad.item())<br>print(&#39;w1&#39;, w1.grad.item())</pre><p>Out:</p><pre>0.7071066904050358<br>----<br>x2 0.5000001283844369<br>w2 0.0<br>x1 -1.5000003851533106<br>w1 1.0000002567688737</pre><p>Great! We got the exact same gradients as in our own implementation.</p><p>The default case for Pytorch is to work with tensors which are multi-dimensional arrays. Hence the need to initialize our parameters as 1-value tensors (for example Tensor([2]) instead of Tensor(2)). We see that we can use arithmetic operations like * and +, and also more complex operations such as torch.tanh(...). Just like in Micrograd, we ran .backwards() on our result node and by doing that ran backpropagation through every node in our graph. We needed to explicitly set requires_grad=True for each node because Pytorch does not calculate the gradients for leaf nodes by default.</p><h3>Part 10. Building a multi-layer perceptron (MLP)</h3><p>Using our main building block — the Value class, we are now going to build our first neural network! We will write a class that will allow us to create simple MLPs. Let’s first create one and then talk about how we use backpropagation to optimize the weights for a specific task.</p><pre>class Neuron:<br>    def __init__(self, nin, layer_idx, neuron_idx):<br>        self.layer_idx = layer_idx<br>        self.neuron_idx = neuron_idx<br>        self.w = [Value(random.uniform(-1, 1), label=f&#39;w^{layer_idx}_({neuron_idx}{i+1})&#39;) for i in range(nin)]<br>        self.b = Value(random.uniform(-1, 1), label=f&#39;b^{layer_idx}_{neuron_idx}&#39;)<br>    <br>    def __call__(self, x):<br>        act = sum((wi*xi for wi, xi in zip(self.w, x)), self.b)<br>        out = act.tanh()<br>        out.label = f&#39;L_{self.layer_idx}{self.neuron_idx}&#39;<br>        return out<br><br>    def parameters(self):<br>        return self.w + [self.b]<br>    <br>class Layer:<br>    def __init__(self, nin, nout, layer_idx):<br>        self.neurons = [Neuron(nin, layer_idx, neuron_idx=i+1) for i in range(nout)]<br>    <br>    def __call__(self, x):<br>        outs = [n(x) for n in self.neurons]<br>        return outs[0] if len(outs) == 1 else outs<br>    <br>    def parameters(self):<br>        return [p for neuron in self.neurons for p in neuron.parameters()]<br><br>class MLP:<br>    def __init__(self, nin, nouts):<br>        sz = [nin] + nouts<br>        self.layers = [Layer(sz[i], sz[i+1], layer_idx=i+1) for i in range(len(nouts))]<br>    <br>    def __call__(self, x):<br>        for layer in self.layers:<br>            x = layer(x)<br>        return x<br>    <br>    def parameters(self):<br>        return [p for layer in self.layers for p in layer.parameters()]</pre><p>Surprisingly, this is all of the code!</p><ul><li>Class Neuron contains code for a computational unit that receives nin inputs and outputs a weighted sum of them plus a bias term, followed by a <em>Tanh</em> operation.</li><li>Class Layer aggregates several neurons such that it takes certain data as input (of size nin) and passes it through nout neurons. We do a small check in the end where if the output contains only one neuron we return it as-is and not in a list.</li><li>Class MLP is the final abstraction in which we aggregate noutinstances of Layerby passing the input data xthrough each of them in succession.</li><li>I added some code that did not appear in the original tutorial by Andrej to display the labels of the weights and neurons so we can make some sense in the drawings we will create later.</li><li>For each class, we added a parametersfunction that collects all trainable parameters. This will come in handy later when we train the network.</li></ul><p>Let’s create an MLP, specifically — this one:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/864/1*In8GFdG4Xe4WcZfCpYiZhw.png" /><figcaption>Fig 3. A basic neural network. Image by author.</figcaption></figure><p>Let’s just go over the notation here:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/183/0*vvKX_t0_gjCbNGZ0.png" /><figcaption>The inputs to the network</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/52/0*5wu3dw3NNz5Q7X6v.png" /><figcaption>The j’th neuron in the i’th layer</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/64/0*7TABZTFxFcDN2OgW.png" /><figcaption>The weight connecting the i’th neuron of layer L-1 to the j’th neuron of the L’th layer</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/38/0*L0qJECrgUb6SUn-h.png" /><figcaption>The bias term of the i’th neuron in the L’th layer</figcaption></figure><p>Our MLP contains 3 inputs, 2 hidden layers of size 2 each, and one output.</p><pre>x = [Value(2.0, label=&#39;in1&#39;),<br>     Value(3.0, label=&#39;in2&#39;),<br>     Value(-1.0, label=&#39;in3&#39;)]<br>n = MLP(3, [2, 2, 1])<br>draw_dot(n(x))</pre><p>Out:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bOfqP6aGG0cfcKKsxluMGQ.png" /><figcaption>output of draw_dot — a simple MLP</figcaption></figure><p>The full computational graph is much more detailed than the simplistic drawing we made of the network because it contains all of the multiplication, addition, and <em>Tanh</em> operations in the network. If we look closely though, we can see that the labels in the detailed drawing make sense. For example — find layer L_11 in the drawing and backtrack to its inputs. You will see that it gets the sum of <em>in1 * w¹_(11), in2 * w¹_(12), in3 * w¹_(13) </em>and <em>b¹_1</em> as inputs — just like our simplistic drawing from before (only slightly less organized).</p><p>Now comes the question — how do we use this neural network? Can we train it? Of course, the answer is — yes! Let’s define some input data points and their respective labels. Our goal will be to train the network to generate the correct label for each input.</p><pre>xs = [<br>    [Value(2.0, label=&#39;in11&#39;),<br>     Value(3.0, label=&#39;in12&#39;),<br>     Value(-1.0, label=&#39;in13&#39;)],<br>    [Value(3.0, label=&#39;in21&#39;),<br>     Value(-1.0, label=&#39;in22&#39;),<br>     Value(0.5, label=&#39;in23&#39;)],<br>    [Value(0.5, label=&#39;in31&#39;),<br>     Value(1.0, label=&#39;in32&#39;),<br>     Value(1.0, label=&#39;in33&#39;)],<br>    [Value(1.0, label=&#39;in41&#39;), <br>     Value(1.0, label=&#39;in42&#39;),<br>     Value(-1.0, label=&#39;in43&#39;)]<br>]<br>ys = [1.0, -1.0, -1.0, 1.0]<br>ypred = [n(x) for x in xs]<br>ypred</pre><p>Out:</p><pre>[Value(data=0.6159125848021643, label=L_31),<br> Value(data=0.6974854432825526, label=L_31),<br> Value(data=0.6258725834364298, label=L_31),<br> Value(data=0.6165492639086749, label=L_31)]</pre><p>We defined 4 input samples and a corresponding label for each of them. Then, we ran theMLP we created previously to generate the output for each sample. We see that our initial guesses are quite off (instead of [1, -1, -1, 1] we got different values). In order to train a neural network we need a loss function. This is a mathematical formula that will be used to estimate the quality of a network&#39;s outputs. We will use the well know mean squared error:</p><pre>loss = sum((yout - ygt)**2 for ygt, yout in zip(ys, ypred))<br>loss</pre><p>Out: Value(data=5.819476097248748)</p><p>In mean squared error, we take the squared difference between each prediction and the corresponding label and sum them all up. The lossobject that we created represents this sum. If we backpropagate through lossusing backward, we will generate the gradient for each one of the network parameters. This gradient will indicate in which direction each parameter must be nudged in order to minimize loss which is our goal. Just for fun — let’s draw the graph represented by loss after we call loss.backward().</p><pre>loss.backward()<br>draw_dot(loss)</pre><p>Out:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*edZyzUAd3W3fy3nBBHi9wA.png" /><figcaption>output of draw_dot — a simple MLP with all forward passes in batch</figcaption></figure><p>This graph is insanely packed. It contains in it all 4 forward passes — one for each input! We can see, though, that each one of our network parameters appears here only once, and each has a gradient that now tells how we should nudge it to minimize the loss. Just as an example, let’s look at the gradient of the first weight of the first neuron of the first layer: n.layers[0].neurons[0].w[0].grad: 0.05664531168775873</p><p>The gradient is positive. This means that to minimize the loss function we need to go in the negative direction of the gradient, i.e. decrease the value of W¹_(11).</p><h3>Part 11. Training the MLP</h3><p>Finally! We are going to train our neural network to fit the dummy data points we showed before. To train our net we need to repeat the forward pass -&gt; loss calculation -&gt; backward pass procedure. We can do it manually, but it makes much more sense to write a simple loop that does this for us:</p><pre>def training_loop(n_steps, learning_rate):<br>    for k in range(n_steps):<br>        # forward pass<br>        ypred = [n(x) for x in xs]<br>        loss = sum((yout - ygt)**2 for ygt, yout in zip(ys, ypred))<br><br>        # backward pass<br>        loss.backward()<br><br>        for p in n.parameters():<br>            p.data -= learning_rate * p.grad<br>        <br>        print(k, loss.data)</pre><p>What is happening here? First, we generate ypredwhich is the forward pass for our 4 examples. Then we generate the loss term which we will calculate the gradients by. Then, we call backwardto calculate gradients for all parameters of the net. Finally, we iterate through all of the network parameters using the parameters()function from the Valueclass and nudge each of them by a small amount (controlled using the specified learning_rate) in the negative direction of the gradient so as to make the loss smaller.</p><p>Let’s just make sure that n.parameters()gives us the expected output:</p><pre>[Value(data=0.45027414801422605, label=w^1_(11)),<br> Value(data=-0.8129525057615291, label=w^1_(12)),<br> Value(data=0.5758542981989572, label=w^1_(13)),<br> Value(data=-0.8030667986239539, label=b^1_1),<br> Value(data=0.5557411827449444, label=w^1_(21)),<br> Value(data=-0.6721847840083467, label=w^1_(22)),<br> Value(data=1.217737844619453, label=w^1_(23)),<br> Value(data=0.09905283405117085, label=b^1_2),<br> Value(data=0.4829801438596318, label=w^2_(11)),<br> Value(data=0.7369938633453851, label=w^2_(12)),<br> Value(data=-0.46073515053706376, label=b^2_1),<br> Value(data=0.27929425462417923, label=w^2_(21)),<br> Value(data=1.7138391095399366, label=w^2_(22)),<br> Value(data=0.7105401730722122, label=b^2_2),<br> Value(data=-2.378848749014902, label=w^3_(11)),<br> Value(data=-0.8342902851970706, label=w^3_(12)),<br> Value(data=-1.2437877048443844, label=b^3_1)]</pre><p>Great! We see here all of the parameters we wish to train, and we don’t see things we don’t wish to train (such as the input data which is assumed to be fixed).</p><p>We are ready to run the training loop using training_loop(n_steps=20, learning_rate=0.01)</p><p>Out:</p><pre>0 5.819476097248748<br>1 5.624553232066405<br>2 5.339641605929825<br>3 5.0032420663990465<br>4 4.6979176273972465<br>5 4.50430643973165<br>6 4.41612726370955<br>7 4.345437217507974<br>8 4.191726524841689<br>9 3.8794059504102045<br>10 3.3966830048541388<br>11 2.8554032260566666<br>12 2.4577023357202674<br>13 2.274646098166827<br>14 2.176592658900446<br>15 2.0400253245956486<br>16 1.8089353320227564<br>17 1.3966145142070974<br>18 0.748901313440497<br>19 0.17987272335400104</pre><pre>ypred = [n(x) for x in xs]<br>ypred</pre><pre>[Value(data=0.9312475693757956, label=L_31),<br> Value(data=-0.9983175660114463, label=L_31),<br> Value(data=-0.895522396196552, label=L_31),<br> Value(data=0.9153783867936369, label=L_31)]</pre><p>Our training procedure works and our ypredis very close to the ground truth. With a bit more training steps we would have gotten even closer.</p><p>But wait…there is a bug here! (in the training loop) Did you spot it? Take a moment to think…</p><p>The bug is subtle but quite common among new neural network practitioners. Before each backward pass, we forgot to clear the gradient information from the computational graph. Remember that as we calculate the gradients for the different components of the graph we use +=instead of =to accumulate gradient information, because a certain node may be used more than once in the graph and to get its gradient we need to sum up all of the contributions to the gradient.</p><p>In the current implementation we forgot to clear the gradient value, so the gradient will get accumulated through subsequent forward passes — which were performed with different network weights! This makes no sense, of course, so a simple fix is in order:</p><pre>def training_loop(n_steps, learning_rate):<br>    for k in range(n_steps):<br>        # forward pass<br>        ypred = [n(x) for x in xs]<br>        loss = sum((yout - ygt)**2 for ygt, yout in zip(ys, ypred))<br><br>        <br>        for p in n.parameters():<br>            p.grad = 0.0<br>            <br>        # backward pass    <br>        loss.backward()<br><br>        for p in n.parameters():<br>            p.data -= learning_rate * p.grad<br>        <br>        print(k, loss.data)<br></pre><p>Before the backward pass, we iterate through all network parameters and assign a value of 0 to them.</p><p>In this toy example, it didn’t really matter, and it even helped the model converge faster because the problem is very easy, and accumulating the gradients was akin to using a massive learning rate (I needed several hundred training steps instead of 20 to reach a similar state of loss), but it is indeed a bug and could lead to crazy results in larger networks.</p><p>Let us run the training loop again after the fix. We will call training_loop(20, 0.01).</p><p>Out:</p><pre>0 5.108927684153838<br>1 4.846717266169518<br>2 4.60037368477449<br>3 4.383622169945541<br>4 4.205267359786554<br>5 4.067195989790671<br>6 3.965330834751441<br>7 3.8924147774168065<br>8 3.840719199945062<br>9 3.803632051028429<br>10 3.7761592608770185<br>11 3.7548059106373577<br>12 3.7372392224767497<br>13 3.7219462205564553<br>14 3.707961320704009<br>15 3.6946723746238628<br>16 3.681690631213215<br>17 3.668766548696825<br>18 3.655736427317908<br>19 3.6424890071677454</pre><p>Unlike before, our training is much slower. This is due to fixing the bug in which the gradients accumulated on each other in each training step instead of resetting. After about 1000 steps our output looks like this:</p><pre>[Value(data=0.9653433264848924, label=L_31),<br> Value(data=-0.9720009732525263, label=L_31),<br> Value(data=-0.9618275470088311, label=L_31),<br> Value(data=0.9694722216419711, label=L_31)]</pre><p>Very close to the ground truth.</p><p>Let’s now wrap up everything we learned here.</p><h3>Part 12. Summary</h3><p>Which question did we answer today?</p><ul><li>What are neural networks? They are mathematical expressions that take weights and input data as parameters and generate some output. The quality of this output is measured via a loss function, which we then use to calculate gradients for all of the network weights (using the chain rule). To minimize the loss function we nudge each parameter by a small amount in the negative direction of the gradient. Hopefully, after many steps of this process, we will be able to minimize the loss function enough, and this means that our network will generate outputs similar to the expected results.</li></ul><p>We created a very basic neural network in this article, for simplicity’s sake, but these networks can get much larger and perform insanely difficult tasks! Such as creating Python code given a free text description, making medical diagnoses given patient records, and much more!</p><p>The incredible thing is that the underlying concepts behind these massive and impressive neural nets are exactly the same as the simple example we showed here today. Congratulations on understanding some pretty important things!</p><p>The notebook containing the code that was shown here — <a href="https://colab.research.google.com/drive/1lG93TNxmdHFLb7xiZquzTRDF5hFcVQkV?usp=sharing">https://colab.research.google.com/drive/1lG93TNxmdHFLb7xiZquzTRDF5hFcVQkV?usp=sharing</a></p><h3>References</h3><ul><li><a href="https://github.com/karpathy/micrograd">https://github.com/karpathy/micrograd</a> — The full version of Micrograd by Andrej Karpathy.</li><li><a href="https://www.youtube.com/watch?v=VMj-3S1tku0">https://www.youtube.com/watch?v=VMj-3S1tku0</a> — “The spelled-out intro to neural networks and backpropagation: building micrograd” — The video on which this blog post is based on.</li><li><a href="https://github.com/karpathy/nn-zero-to-hero/tree/master/lectures/micrograd">https://github.com/karpathy/nn-zero-to-hero/tree/master/lectures/micrograd</a> — Notebooks from Andrej’s video.</li><li><a href="https://colab.research.google.com/drive/1lG93TNxmdHFLb7xiZquzTRDF5hFcVQkV?usp=sharing">https://colab.research.google.com/drive/1lG93TNxmdHFLb7xiZquzTRDF5hFcVQkV?usp=sharing</a> — Notebook I created in the process of making this blog post.</li><li><a href="https://colab.research.google.com/drive/1FPTx1RXtBfc4MaTkf7viZZD4U2F9gtKN?usp=sharing">https://colab.research.google.com/drive/1FPTx1RXtBfc4MaTkf7viZZD4U2F9gtKN?usp=sharing</a> — Notebook by Andrej containing follow-up exercises to this lesson.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d54cd8fc61e4" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[VALL-E — The Future of Text to Speech?]]></title>
            <link>https://medium.com/data-science/vall-e-the-future-of-text-to-speech-d090b6ede07b?source=rss-d2d1ff8f0490------2</link>
            <guid isPermaLink="false">https://medium.com/p/d090b6ede07b</guid>
            <category><![CDATA[text-to-speech]]></category>
            <category><![CDATA[deep-learning]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[Elad Rapaport]]></dc:creator>
            <pubDate>Fri, 14 Apr 2023 17:30:49 GMT</pubDate>
            <atom:updated>2023-04-14T17:30:49.402Z</atom:updated>
            <content:encoded><![CDATA[<h3>VALL-E — The Future of Text to Speech?</h3><h4>A paper walkthrough of the new text-to-speech model by Microsoft Research</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CUko1OJW1K8FNw8CNmEmvQ.png" /><figcaption>DALL-E 2: a record player that receives text on one side, and outputs sound waves on the other side. digital art.</figcaption></figure><p>Hello readers,</p><p>In this article, we will dive deep into a new and exciting text-to-speech model developed by Microsoft Research, called VALL-E. The paper presenting the work has been released on Jan. 5, 2023, and since then has been gaining much attention online. It is worth noting that as of writing this article, no pre-trained model has been released and the only option currently to battle-test this model is to train it by yourself.</p><p>Nevertheless, the idea presented in this paper is novel and interesting and worth digging into, regardless of whether I can immediately clone my voice with it or not.</p><p>This article will be organized as follows:</p><ul><li>Part 1 — Introduction to Text to Speech, Basic Concepts</li><li>Part 2 — VALL-E: Text to Speech as a Language Model</li><li>Part 3 — Encodec: The Workhorse Behind VALL-E</li><li>Part 4 — Problem formulation and training of VALL-E</li><li>Part 5— Some Coding</li><li>Part 6— Conclusions &amp; Thoughts Ahead</li></ul><h3>Part 1 — Introduction to Text to Speech, Basic Concepts</h3><p>The technology of text-to-speech is not new and has been around since the “Voder” — the first electronic voice synthesizer from Bell Labs in 1939 which required manual operation. Since then, the field has seen incredible developments and up until ~2017, the dominant technology was concatenative speech synthesis. This technology is based on the concatenation of pre-recorded speech segments to create intelligible speech. Although this technology can produce lifelike results, its drawbacks are obvious — it cannot generate new voices which don’t exist in the pre-recorded database, and it cannot generate speech with a different tone or emotion.</p><p>Fast-forward to the era of deep learning. Nowadays, the dominant strategy in text-to-speech synthesis is summarized in Figure 1. Let’s go over its different parts.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*r1RZ-R4VlcODFoqkGtPPZw.png" /><figcaption>Figure 1. A model neural text-to-speech pipeline. Image by author.</figcaption></figure><ul><li>First, we have a phonemizer that transforms text into phonemes. Phonemes are a textual representation of the pronunciation of words (for example — the word tomato will have different phonemes in an American and British accent), and this representation helps the downstream model achieve better results.</li><li>Afterward, we have an acoustic model which transforms these phonemes into a Mel spectrogram, which is a representation of audio in the time X frequency domain. A spectrogram is achieved by applying a short Fourier transform (STFT) on overlapping time windows of a raw audio waveform (here is an excellent explanation about the Mel spectrogram — <a href="https://medium.com/analytics-vidhya/understanding-the-mel-spectrogram-fca2afa2ce53">https://medium.com/analytics-vidhya/understanding-the-mel-spectrogram-fca2afa2ce53</a>). Of course in this case the spectrogram is being created by a statistical model, as no input audio exists in real-time text-to-speech. Examples of recent model architectures include Tacotron2, DeepVoice 3, and TransformerTTS.</li><li>The final stage is the conversion of the Mel spectrogram into a waveform. A waveform is usually sampled at 24/48 kHz, where each sample is digitized into a 16-bit number. These numbers represent the amount of air pressure at each moment in time, which is the sound we eventually hear in our ears. Why can’t we just deterministically convert the spectrogram into a waveform? Because it requires major upsampling in the time domain which requires us to create information that doesn’t explicitly exist in the spectrogram, and also because spectrograms don’t contain phase information (only frequency). So, as in the conversion of phonemes to a Mel spectrogram, here as well we need a statistical model to convert the spectrogram into a waveform and these models are called Vocoders. Examples of Vocoders include WaveNet, WaveRNN, and MelGAN.</li></ul><p>Additionally, there are recent models such as VITS and YourTTS, which employ an end-to-end model to generate waveforms from text input. Another example of such an end-to-end system is a paper titled “End-to-End Adversarial Text-to-Speech” by Deepmind (which is excellently explained by Yannic Kilcher here — <a href="https://www.youtube.com/watch?v=WTB2p4bqtXU">https://www.youtube.com/watch?v=WTB2p4bqtXU</a>). In this paper, they employ a GAN-like training procedure to produce realistic speech sound waves. They also need to tackle the alignment problem, which is the degree to which word utterances in the generated samples align in time with the same word utterances in the ground truth samples. This problem does not “solve on its own” and requires explicit handling in the model architecture.</p><p>The main drawback of these end-to-end TTS models is their incredible complexity. Text and speech are such different modalities, and this requires complex models which tackle problems such as alignment, speaker identity, and language in an explicit manner, making these models highly complex. The charm of VALL-E, which we will soon dive into, is that it takes the relative simplicity of generative language models and employs them creatively in the field of speech generation. For people like me who are new to the field of TTS and speech in general and have some experience in NLP, it allows a good entry point into this fascinating field.</p><p>This short overview did not do justice to the immense field of TTS, which one can spend a lifetime studying and understanding (I do encourage you to dive a bit deeper). Yet, we are here today to talk about VALL-E, so allow me to jump straight to it.</p><h3>Part 2 — VALL-E: Text to Speech as a Language Model</h3><p>As in other text-to-speech systems, the input to VALL-E is phonemicized text, and the output is the corresponding sound waveform. Additionally, VALL-E employs a prompting mechanism in which a 3-second audio sample is fed as additional input to the model. This allows the generation of a speech utterance of the input text which is conditioned on the given audio prompt — in practice, this means the ability to perform zero-shot speech generation, which is the generation of speech from a voice unseen in the training data. The high-level structure of VALL-E is presented in Figure 2.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tlTcnN_75TRt30e60bttOg.png" /><figcaption>Figure 2. The high-level structure of VALL-E. Image taken from the original paper [1].</figcaption></figure><p>Let’s understand what happens in this pipeline. First, we have a phoneme conversion of the text, which is standard procedure as we understood already, and doesn’t require any learning mechanism. In order to process these phonemes by the model we have a phoneme embedding layer that takes as input a vector of indices into the phoneme vocabulary and outputs a matrix of embeddings corresponding to the input indices.</p><p>The 3-second acoustic prompt, which the output speech is conditioned on, is fed into an audio codec encoder. In VALL-E they use a pre-trained audio encoder for this — Encodec (developed by Facebook Research — <a href="https://arxiv.org/abs/2210.13438">https://arxiv.org/abs/2210.13438</a>). Encodec takes as input a waveform of speech and outputs a compressed discrete representation of it via recursive vector quantization (RVQ) using an encoder-decoder neural architecture. We will dive into Encodec in Part 3 of this article, but for now, let’s just assume that it outputs a discrete representation of the audio signal by splitting it into fixed time windows and assigning each window a representation from a known vocabulary of audio embeddings (conceptually, very similar to word embeddings).</p><p>Once the model receives these two inputs, it can act as an autoregressive language model and output the next discrete audio representation. Because the audio representations come from a fixed which was learned by Encodec, we can think of this simply as predicting the next word in a sentence out of a fixed vocabulary of words (a fixed vocabulary of sound representations, in our case). After these sound representations are predicted they are transformed back into the original waveform representation using the Decoder part of the Encodec model.</p><p>In Figure 3 we compare the pipeline of VALL-E to the traditional neural TTS pipeline. We see that the main difference is the intermediate representation of audio. In VALL-E they gave up on the Mel spectrogram and used the representation created by the Encodec model. It is worth noting though that under the hood Encodec uses a spectrogram representation as well, so it is still somewhat in use in this architecture, albeit less prominently.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-_F5SGx8jg1QuyyrUhd05Q.png" /><figcaption>Figure 3. The VALL-E pipeline VS the traditional neural TTS pipeline. Image by author.</figcaption></figure><p>In the results section of the VALL-E paper, they have shown that they outperform the previous state-of-the-art zero-shot TTS model, YourTTS, on the LibriSpeech data on several metrics which include human-based evaluations such as similarity mean option score (SMOS) and algorithm-based evaluations such as word error rate (WER). In an interesting ablation study, they show that the phoneme prompt contributes to the content of generation (by reducing WER) and the audio prompt contributes to speaker similarity (by improving a speaker similarity metric).</p><p>We will now dive into the Encodec model which is responsible for converting audio to discrete tokens and back and is the enabler for using a language model approach to audio generation in this paper.</p><h3>Part 3 — Encodec: The Workhorse Behind VALL-E</h3><p>In Figure 4 we can see the Encodec architecture. It is an encoder-decoder architecture that learns a condensed representation of the audio signal via the task of reconstruction. Let’s go over its different parts to understand what is going on under the hood.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XzxoSBeFiFq5zLD0KaJyIg.png" /><figcaption>Figure 4. The Encodec architecture. Image taken from the original paper [2].</figcaption></figure><p>On the far left we have our original waveform, which is sampled at 24/48 kHz, and each sample is represented by 16 bits (65536 options). The raw signal gets passed into the Encoder which includes 1D convolution operations for downsampling and a two-layer LSTM for sequence modeling. The output of the encoder is 75/150 latent timesteps (compare this to the original 24/48K!), with a depth dimension of 128.</p><p>The decoder is simply a mirrored version of the encoder, using transposed convolutions in order to upsample the latent space and construct the audio waveform (here is a good explanation on transposed convolutions <a href="https://towardsdatascience.com/what-is-transposed-convolutional-layer-40e5e6e31c11">https://towardsdatascience.com/what-is-transposed-convolutional-layer-40e5e6e31c11</a>).</p><p>The interesting bit here is, of course, the quantizer. How does Encodec quantize the continuous domain of sound? Using a technique called residual vector quantization (RVQ) which consists of projecting an input vector onto the closest entry in a codebook of a given size. Let’s break that sentence down. First, what is a codebook?</p><p>In the case of VALL-E, a codebook is a dictionary of vectors of size 1024, where each entry represents a vector of size 128. Our goal in vector quantization is to map a certain vector to the closest vector in the codebook (by Euclidean distance, for example), thereafter it can be represented by the index of that vector in the codebook (assuming everyone has access to the codebook). Of course, in this way, we lose a lot of information. What if no vector in the codebook accurately resembles our vector? Hence the “residual” in RVQ!</p><p>In Figure 5, I show how a vector is quantized using residual vector quantization. In the example, we have 3 codebooks. The input vector is compared to each of the vectors in the first codebook and assigned to the closest one (C1,1). Then, the residual between the C1,1 and the input is calculated and we try to match the residual to the next codebook, and so on until we reach the end of our codebooks. The final RVQ representation is the indices that were matched in each of the codebooks (1, 3, 2 in our example). This encoding method is extremely efficient. If we have 8 codebooks where each contains 1024 entries — we can represent 1024⁸=1.2e+24 different vectors using only 1024*8=8192 numbers! Of course, the sender and receiver must hold the same codebook for this quantization method to work. If you want to learn more about RVQ, such as how the codebooks are trained, I recommend reading another paper that Encodec is based on called SoundStream — <a href="https://arxiv.org/abs/2107.03312">https://arxiv.org/abs/2107.03312</a> (yes, this is a rabbit hole).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/567/1*C910t1WYaBhNlnfluHqM-Q.png" /><figcaption>Figure 5. Example of Residual Vector Quantization. Image by author.</figcaption></figure><p>Back to the Encodec pipeline in Figure 4, let’s notice 3 additional details which are relevant to its training procedure:</p><ol><li>Mel spectrograms are created both from the input audio and from the generated audio. These spectrograms are compared and the signal from the comparison is used as a loss to direct the model training.</li><li>Several discriminators are used in order to compare a short-time Fourier transform (STFT) of the original and synthetic waveform. This GAN loss gives a different signal than the Mel spectrogram comparison and was found useful for Encodec.</li><li>The quantizer contains transformers that are used for further compression of the audio signal. This is <strong>not</strong> the transformer in VALL-E that predicts the next token of speech, as confusing as it may be. For further understanding of the transformers in Encodec, I recommend reading the paper or watching the video by Aleksa Gordic — <a href="https://www.youtube.com/watch?v=mV7bhf6b2Hs">https://www.youtube.com/watch?v=mV7bhf6b2Hs</a>.</li></ol><p>Let’s summarize what we know so far. VALL-E is a text-to-speech model that resembles language models in its operational mode, such that it predicts the next discrete audio token for a given prompt, which consists of phonemicized text and audio input. These discrete tokens are learned by another model called Encodec (which itself is based on SoundStream) that uses an encoder-decoder architecture with residual vector quantization to convert audio into discrete codes.</p><h3>Part 4 — Problem formulation and training of VALL-E</h3><p>VALL-E contains two transformer models which are used to process the input data (phonemicized text and audio) — an autoregressive (AR) transformer that attends only to past data, and a non-autoregressive (NAR) transformer that attends to all points in time. Let’s see why.</p><p>Eight different codebooks are used in VALL-E as part of the Encodec model, where each codebook consists of 1024 entries. The codes from the first quantizer (codebook) are processed by the AR model according to this Equation 1. Let’s first clarify some terminology here:</p><ul><li>C represents the generated output — as discrete audio codes</li><li>C~ is the 3-second input acoustic prompt</li><li>x is the input text as a phoneme sequence</li><li>C:,₁ represents data from the first quantizer/codebook for C</li></ul><p>So, Equation 1 shows that the output for the first quantizer is conditioned on the input data, and on the previous timesteps’ outputs for the first quantizer (just like an autoregressive language model).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/790/1*xITlIsqz_DLPxl2N5iv8UA.png" /><figcaption>Equation 1. The autoregressive model — is applied to the first quantizer. Image taken from the original paper [1].</figcaption></figure><p>In Equation 2 we see the generation of codes for quantizers 2 to 8. Unlike the previous case, here the output for each quantizer is conditioned on all of the timesteps from the previous quantizers (when calculating codes for quantizer #7, the model is conditioned on data generated for quantizers 1 to 6). Unlike the AR model, this allows parallel generation of all timesteps in a single quantizer because it is only dependent on previous quantizer codes and not on previous timesteps of the same quantizer. The authors stressed this point because fast inference is especially important in text-to-speech models which need to generate speech in real-time scenarios.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6LakhdpKa6XOiTMcAaZCNQ.png" /><figcaption>Equation 1. The non-autoregressive model — is applied to the second to eight quantizers. Image taken from the original paper [1].</figcaption></figure><p>Equations 1 and 2 are visually depicted in Figure 6, which shows the AR and NAR models together and highlights the differences between them. We see that the AR transformer is used to predict only C:,₁ which are the tokens for the first quantizer. While doing so, it attends to the previous tokens it has generated. the NAR transformer attends to the previous quantizers, and not to previous timesteps (the previous tokens of the current quantizer are not available in the NAR model).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zZmUzjNyvSa3a-b-c7bdXQ.png" /><figcaption>Figure 6. AR and NAR models in VALL-E. Image taken from the original paper [1].</figcaption></figure><p>VALL-E has been trained on 60K hours of audio from the LibriLight dataset, containing 7000 distinct speakers (which is over 100 times more data than previous state-of-the-art). The dataset is audio-only, hence for labeling an automatic speech recognition model was used. The Encodec model is used as a pre-trained model and no fine-tuning is performed on it for VALL-E as far as I could understand.</p><p>For training, random 10–20 second samples were taken from LibriLight. For the acoustic prompt, another 3 seconds were taken from the same utterance. They used 16 Tesla V-100 GPUs to train the model, which is very modest compared to large SOTA language models!</p><p>We learned about the procedure and data, now let’s try to use the unofficial Pytorch implementation of VALL-E in GitHub.</p><h3>Part 5 — Some Coding</h3><p>VALL-E doesn’t have an official implementation on GitHub, so for my experimentations, I will rely on the unofficial version which was released — <a href="https://github.com/enhuiz/vall-e">https://github.com/enhuiz/vall-e</a>. Moreover, no model checkpoint has been released so you have to train it from scratch.</p><p>There is also a Google Colab notebook to follow along with a simple training example — <a href="https://colab.research.google.com/drive/1wEze0kQ0gt9B3bQmmbtbSXCoCTpq5vg-?usp=sharing">https://colab.research.google.com/drive/1wEze0kQ0gt9B3bQmmbtbSXCoCTpq5vg-?usp=sharing</a>. In this example, they overfit the model on a single utterance of “hello world” and they show that the model is able to reproduce this single utterance. I was interested in two things:</p><ol><li>I wanted to replicate their “hello world” experiment with my own voice, just to see that the pipeline is working properly</li><li>I wanted to replicate the experiment done by James Skelton from Paperspace — <a href="https://blog.paperspace.com/training-vall-e-from-scratch-on-your-own-voice-samples/">https://blog.paperspace.com/training-vall-e-from-scratch-on-your-own-voice-samples/</a>, where he trained a model on a very small subset of his own recordings, and managed to replicate his voice with it (on something he already recorded)</li></ol><p>Why the limited experiments? Because training this model from scratch takes many resources which I don’t currently have, plus I assume a pre-trained model will be released sooner or later.</p><p>So how did I succeed? I managed to replicate the “hello world” experiment, but unfortunately, I didn’t manage to replicate the Paperspace experiment — I just got a model which creates a garbled sound that vaguely reminds my voice. This is probably because of a lack of resources (I am training it on a Google Colab instance which times out after 12 hours). But still, I want to go over the process with you. My version of the VALL-E notebook is here — <a href="https://colab.research.google.com/drive/1NNOsvfiOfGeV-BBgGkwf0pyGAwAgx3Gi#scrollTo=SbWtNBVg_Tfd">https://colab.research.google.com/drive/1NNOsvfiOfGeV-BBgGkwf0pyGAwAgx3Gi#scrollTo=SbWtNBVg_Tfd</a>.</p><p>Once you run the following line in the Colab notebook —</p><pre>!git clone --recurse-submodules https://github.com/enhuiz/vall-e.git</pre><p>You will see a directory called vall-e in your file browser. The path content/vall-e/data/testcontains the data for the “hello world” experiment. Notice that it contains two files because for some reason it breaks with only one. To replicate this experiment, simply delete the files in the data directory using !rm content/vall-e/data/test/*, record yourself saying “Hello world”, and save it as two .wav files with different names. Put the .wav files in the data directory including two text files containing the words “hello world” (the text files should have the same names as the .wav files with a.normalized.txt suffix).</p><p>Following that, you will run these two cells:</p><pre>!python -m vall_e.emb.qnt data/test</pre><pre>!python -m vall_e.emb.g2p data/test</pre><p>The first cell will run the Encodec model on your own data and perform quantization, just as we discussed earlier. The second cell will convert the text “hello world” into phonemes.</p><p>Afterward, the processed data is ready and you can run the cells that operate the training procedure. There is separate training for the NAR and AR models (remember that as we saw earlier, the NAR model training is dependent on the AR model, but the AR model uses and produces only the first quantizer data, thus independent of the NAR model).</p><pre>!python -m vall_e.train yaml=config/test/ar.yml</pre><pre>!python -m vall_e.train yaml=config/test/nar.yml</pre><p>After the model has finished training you will run this cell:</p><pre>!mkdir -p zoo<br>!python -m vall_e.export zoo/ar.pt yaml=config/test/ar.yml<br>!python -m vall_e.export zoo/nar.pt yaml=config/test/nar.yml</pre><p>Which saves the latest model checkpoint (that has been automatically created) into a directory called zoo.</p><p>Finally, you will perform inference with the model using:</p><pre>!python -m vall_e &#39;hello world&#39; /content/vall-e/data/test/hello_world.wav toy.wav --ar-ckpt zoo/ar.pt --nar-ckpt zoo/nar.pt</pre><p>This will run the model with a text prompt of “Hello world”, and an audio prompt of the same utterance. It will save the generated sample as toy.wav which you can then listen to using:</p><pre>from IPython.display import Audio<br>Audio(&#39;toy.wav&#39;)</pre><p>And that’s it! You created your own VALL-E “Hello world”. Unless you have many computing resources, it is probably best to wait for a pre-trained model to come around to actually make use of this model further.</p><h3>Part 6 — Conclusions &amp; Thoughts Ahead</h3><p>In this article, we saw VALL-E, a new text-to-speech architecture by Microsoft Research. VALL-E generates audio in a language-model-like manner, which differentiates it from recent state-of-the-art methods that are usually end-to-end or follow a text-&gt;spectrogram-&gt;waveform creation pipeline.</p><p>We also talked about the Encodec model, which performs audio quantization and is used as a pre-trained model in the training of VALL-E. Encodec is fascinating in itself and manages to create super-condensed audio representations using residual vector quantization. The creators of VALL-E leveraged this feature and built a generative “language” model on top of this quantization.</p><p>Finally, we saw some code and replicated the “hello world” experiment from the unofficial code with our own voice. The official code for this paper hasn’t been released, nor has a model checkpoint been released. It would be interesting to see and use a pre-trained model for VALL-E, which I assume will turn up sooner or later. Nevertheless, this was an interesting learning journey.</p><p>See you next time!</p><p>Elad</p><h3>References</h3><p>[1] <a href="https://arxiv.org/abs/2301.02111">https://arxiv.org/abs/2301.02111</a> — The VALL-E paper (Neural Codec Language Models are Zero-Shot Text to Speech Synthesizers)</p><p>[2] <a href="https://arxiv.org/abs/2301.02111">https://arxiv.org/abs/2301.02111</a> — The Encodec paper (High Fidelity Neural Audio Compression)</p><p>[3] <a href="https://wiki.aalto.fi/display/ITSP/Concatenative+speech+synthesis">https://wiki.aalto.fi/display/ITSP/Concatenative+speech+synthesis</a> — Explanation on concatenative speech synthesis</p><p>[4] <a href="https://www.youtube.com/watch?v=aLBedWj-5CQ&amp;t=1s">https://www.youtube.com/watch?v=aLBedWj-5CQ&amp;t=1s</a> — Deep dive into speech synthesis meetup (HuggingFace)</p><p>[5] <a href="https://www.youtube.com/watch?v=MA8PCvmr8B0">https://www.youtube.com/watch?v=MA8PCvmr8B0</a> — Pushing the frontier of neural text to speech (Microsoft Research)</p><p>[6] <a href="https://www.youtube.com/watch?v=G9k-2mYl6Vo&amp;t=5593s">https://www.youtube.com/watch?v=G9k-2mYl6Vo&amp;t=5593s</a> — Excellent video by John Tan Chong Min about VALL-E</p><p>[7] <a href="https://www.youtube.com/watch?v=mV7bhf6b2Hs">https://www.youtube.com/watch?v=mV7bhf6b2Hs</a> — Excellent video by Aleksa Gordic about Encodec</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d090b6ede07b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/vall-e-the-future-of-text-to-speech-d090b6ede07b">VALL-E — The Future of Text to Speech?</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[Improving Hebrew Q&A Models via Prompting]]></title>
            <link>https://medium.com/data-science/improving-hebrew-q-a-models-via-prompting-20f5fb5a2f36?source=rss-d2d1ff8f0490------2</link>
            <guid isPermaLink="false">https://medium.com/p/20f5fb5a2f36</guid>
            <category><![CDATA[openai]]></category>
            <category><![CDATA[large-language-models]]></category>
            <category><![CDATA[naturallanguageprocessing]]></category>
            <dc:creator><![CDATA[Elad Rapaport]]></dc:creator>
            <pubDate>Fri, 17 Mar 2023 14:46:35 GMT</pubDate>
            <atom:updated>2023-03-17T14:46:35.935Z</atom:updated>
            <content:encoded><![CDATA[<h3>Improving Hebrew Q&amp;A Models via Intelligent Prompting</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sAgujwyU51JMkOG5y4_RLw.png" /><figcaption>DALL-E 2: “A robot dressed in a robe writing biblical sentences in Hebrew. Digital art.”</figcaption></figure><p>Hi all,</p><p>I am sharing a short project I worked on that involves improving the performance of the text-davinci-003 model by OpenAI using intelligent prompting. I will start by saying that this work is inspired by the excellent video tutorials by <a href="https://www.youtube.com/@jamesbriggs/featured">James Briggs</a> — (specifically this one — <a href="https://youtu.be/dRUIGgNBvVk">https://youtu.be/dRUIGgNBvVk</a>) and much of the code I used is taken from his examples as well.</p><p>The outline of this article is as follows:</p><ol><li>Intro — Unique and Proprietary data in Large Language Models (LLMs)</li><li>Problem Statement — Answering Questions in Hebrew</li><li>Solution Suggestion — Prompting &amp; Querying</li><li>Experiment — Question Answering in Hebrew</li><li>Conclusions</li></ol><p>A Google Colab notebook with the complete code is here — <a href="https://colab.research.google.com/drive/1_UqPHGPW1yLf3O_BOySRjf3bWqMe6A4H#scrollTo=4DY7XgilIr-H">https://colab.research.google.com/drive/1_UqPHGPW1yLf3O_BOySRjf3bWqMe6A4H#scrollTo=4DY7XgilIr-H</a></p><h3>Part 1. Problem Statement — Unique and Proprietary data in Large Language Models</h3><p>The main official language in Israel is Hebrew, and as an Israeli, I am interested in improving the performance of large language models in Hebrew. It is a widely known fact that the performance of ChatGPT in Hebrew could be better relative to its performance in English, despite recent improvements. In this article, though, Hebrew is only an allegory to a source of data that is under-represented in a large language model and this article is going to deal with an attempt to assist an LLM in yielding useful results which are based on such data.</p><p>One option, which was common before the era of very large language models, was to perform additional training on the model with the new data/task. When the models were at a scale of millions of parameters this option was a viable approach for many people and use cases and was achievable using common consumer hardware.</p><p>In the past several years there has been an explosion in model sizes for large language models, which nowadays reach hundreds of billions of parameters. On the other hand, consumer hardware has not seen such a massive efficiency explosion. Thus, the option for fine-tuning these models has been taken off the table for most users and organizations.</p><p>ChatGPT is widely known already and it achieves remarkable performance on many tasks and is proving to be an invaluable AI assistant. Yet still, if I want to use this technology to assist a person speaking in a rare language, or to assist a company with proprietary data which is not available online — chances are that ChatGPT will fail for obvious reasons of not seeing the data beforehand. So how do we make use of special/proprietary data in these models, without requiring millions of dollars for training using special hardware? One option is prompting via querying, which we will explore today.</p><h3><strong>Part 2. </strong>Problem Statement — Answering Questions in Hebrew</h3><p>We will use the OpenAI API to answer three questions in Hebrew, which were inspired by the <a href="https://rajpurkar.github.io/SQuAD-explorer/explore/v2.0/dev/">SQuAD</a> dataset and translated into Hebrew. The following code snippet shows the questions, expected answers, and English translations.</p><pre>questions_answers = [<br>     {&#39;subject&#39;: &#39;אוסטרליה&#39;,<br>      &#39;subject_translation&#39;: &#39;Australia&#39;,<br>      &#39;question&#39;: &quot;איזו עיר בויקטוריה נחשבת לבירת הספורט של אוסטרליה?&quot;,<br>      &#39;answer&#39;: &quot;מלבורן&quot;,<br>      &#39;question_translation&#39;: &quot;Which city in Victoria is considered the sporting capital of Australia?&quot;,<br>      &#39;answer_translation&#39;: &quot;Melbourne&quot;},<br>     {&#39;subject&#39;: &#39;אוניברסיטה&#39;,<br>      &#39;subject_translation&#39;: &#39;University&#39;,<br>      &#39;question&#39;: &quot;איזה נהר ממוקם בקרבת אוניברסיטת הארווארד?&quot;,<br>      &#39;answer&#39;: &quot;נהר צ&#39;ארלס&quot;,<br>      &#39;question_translation&#39;: &quot;Which river is located in the vicinity of Harvard University?&quot;,<br>      &#39;answer_translation&#39;: &quot;Charles River&quot;},<br>     {&#39;subject&#39;: &#39;המוות השחור&#39;,<br>      &#39;subject_translation&#39;: &#39;The black plague&#39;,<br>      &#39;question&#39;: &quot;כמה אנשים ברחבי העולם נספו בעקבות המוות השחור?&quot;,<br>      &#39;answer&#39;: &quot;בין 75 ל200 מיליון איש&quot;,<br>      &#39;question_translation&#39;: &quot;How many people in the world died from the black plague?&quot;,<br>      &#39;answer_translation&#39;: &quot;Between 75 and 200 million&quot;},<br>]</pre><p><strong>Disclaimer — the recent version of ChatGPT proves to yield very good answers for these queries (in Hebrew). The point of this article is not to “compete” with it, but rather to show a solution to a problem that might arise with other models/data.</strong></p><p>We will use the text-davinci-003 model to answer the questions using this code, which will call the OpenAI API:</p><pre>from googletrans import Translator<br>import openai<br><br>def complete(prompt):<br>    # query text-davinci-003<br>    res = openai.Completion.create(<br>        engine=&#39;text-davinci-003&#39;,<br>        prompt=prompt,<br>        temperature=0,<br>        max_tokens=400,<br>        top_p=1,<br>        frequency_penalty=0,<br>        presence_penalty=0,<br>        stop=None<br>    )<br>    return res[&#39;choices&#39;][0][&#39;text&#39;].strip()<br><br>def print_question_answer(question, query, expected_answer, question_translation, expected_answer_translation):<br>  translator = Translator()<br>  answer = complete(query)<br>  print(f&#39;Question: {question}\n&#39; \<br>        f&#39;Question (translation): {question_translation}\n&#39; \<br>        f&#39;Answer: {answer}\n&#39; \<br>        f&#39;Answer (translation): {translator.translate(answer).text}\n&#39; \<br>        f&#39;Expected Answer: {expected_answer}\n&#39; \<br>        f&#39;Expected Answer (translation): {expected_answer_translation}&#39;<br>        )<br><br>for i, qa_dct in enumerate(questions_answers):<br>  print(f&#39;####################&#39;)<br>  print(f&#39;Question #{i + 1}:&#39;)<br>  print_question_answer(qa_dct[&#39;question&#39;],<br>                        qa_dct[&#39;question&#39;],<br>                        qa_dct[&#39;answer&#39;],<br>                        qa_dct[&#39;question_translation&#39;],<br>                        qa_dct[&#39;answer_translation&#39;],<br>                        )</pre><p>Output:</p><pre>####################<br>Question #1:<br>Question: איזו עיר בויקטוריה נחשבת לבירת הספורט של אוסטרליה?<br>Question (translation): Which city in Victoria is considered the sporting capital of Australia?<br>Answer: סידני.<br>Answer (translation): Sydney.<br>Expected Answer: מלבורן<br>Expected Answer (translation): Melbourne<br>####################<br>Question #2:<br>Question: איזה נהר ממוקם בקרבת אוניברסיטת הארווארד?<br>Question (translation): Which river is located in the vicinity of Harvard University?<br>Answer: הנהר הקרוב ביותר לאוניברסיטת הארווארד הוא נהר הקונגו.<br>Answer (translation): The closest river to Harvard University is the Congo River.<br>Expected Answer: נהר צ&#39;ארלס<br>Expected Answer (translation): Charles River<br>####################<br>Question #3:<br>Question: כמה אנשים ברחבי העולם נספו בעקבות המוות השחור?<br>Question (translation): How many people in the world died from the black plague?<br>Answer: בעקבות המוות השחור של ג&#39;ורג&#39; פלאי, הפגנו אלפי אנשים ברחבי העולם בכדי להגיב לפעולות הפולישיות של המשטרה האמריקאית. אלפי אנשים הפגינו בערים בכל הארצות האמריקאיות, כמו ג&#39;קסוןביל, ניו יורק, לוס אנג&#39;לס, פילדלפיה, אתרוג, אוסטין, סנט לואיס, אוסטין, אינדיאנה, מישיגן, אורלנדו, קליפורניה, אוהיו, אריזונה, אוקלהומה, ארצות הברית החדשות ועוד. אף אנשים<br>Answer (translation): Following the black death of George Flay, thousands of people demonstrated around the world to respond to the police actions of the American police. Thousands of people demonstrated in cities in all American countries, such as Jacksonville, New York, Los Angeles, Philadelphia, Etrog, Austin, St. Louis, Austin, Indiana, Michigan, Orlando, California, Ohio, Arizona, Oklahoma, New United States and more . no people<br>Expected Answer: בין 75 ל200 מיליון איש<br>Expected Answer (translation): Between 75 and 200 million</pre><p>We see that text-davinci-003 is very bad at answering these questions out of the box. It gets all three questions wrong and produces some ridiculous answers for the second and third questions.</p><p>So the question is asked — how do we solve this? How can we get good answers from text-davinci-003 without having to fine-tune it on a bunch of Hebrew data?</p><h3>Part 3. Solution Suggestion — Prompting &amp; Querying</h3><p>As I wrote in the introduction, one way to add information to the LLM without fine-tuning is prompting, which is simply including relevant information in the input to the model. But including relevant information for each query can be a very tedious and time-consuming task, can we automate this?</p><p>Of course, we can! Enter prompting via querying. We will take a fairly small dump of Wikipedia in Hebrew, which definitely contains answers to our questions. We will split this dump into lines, embed each of these lines using an embedding model by OpenAI, and insert these embeddings into a vector database called Pinecone DB. Then, for each question we ask the model, we will search the vector database for bits of information that are semantically similar to the question and append them to the model input as additional information. This procedure is shown in figure 1. Hopefully, the model will be able to make use of the provided context and yield correct answers to our questions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0fq5jgOlve0VFofmHdC42g.png" /><figcaption>Figure 1. Prompting with querying</figcaption></figure><p>Inspired by Jason Briggs, I will show you a prompt template that will help us generate intelligible prompts to text-davinci-003 and hopefully, make it answer our three previous questions correctly.</p><h3>Part 4. Experiment — Question Answering in Hebrew</h3><p>First things first — we need to populate our vector database with relevant information. For this purpose we are going to use a small Hebrew Wikipedia dump which I downloaded from here — <a href="https://u.cs.biu.ac.il/~yogo/hebwiki/">https://u.cs.biu.ac.il/~yogo/hebwiki/</a> (available under a Creative Commons Attribution-ShareAlike 4.0 International License).</p><pre>with open(&#39;/content/drive/MyDrive/Datasets/hebrew_wikipedia/full.txt&#39;) as f:<br>  hebrew_wiki = f.readlines()<br><br>openai_price_per_1k = 0.00004<br>n_tokens = sum([len(x) for x in hebrew_wiki])<br>print(f&#39;This hebrew wikipedia dataset contains {len(hebrew_wiki)} sentences&#39;)<br>print(f&#39;In total it amounts to {n_tokens} tokens&#39;)<br>print(f&#39;With the current pricing of openAI it will cost {(openai_price_per_1k * n_tokens) / 1000:.3f}$ to embed everything&#39;)</pre><pre>This hebrew wikipedia dataset contains 3833140 sentences<br>In total it amounts to 380692471 tokens<br>With the current pricing of openAI it will cost 15.228$ to embed everything</pre><p>For general information, I calculated how much it will cost to embed the whole Wikipedia dump using the OpenAI API — not too bad! But still, for our purposes we only need specific subjects so we can save some money by choosing more particular sentences to embed:</p><pre>def get_lines_per_subject(hebrew_wiki, subject, subject_translation):<br>  subject_lines = [(i, x) for i, x in enumerate(hebrew_wiki) if subject in x]<br>  n_tokens = sum([len(x[1]) for x in subject_lines])<br>  print(f&#39;There are {len(subject_lines)} sentences that have the words {subject_translation} in them&#39;)<br>  print(f&#39;In total it amounts to {n_tokens} tokens&#39;)<br>  print(f&#39;With the current pricing of openAI it will cost {(openai_price_per_1k * n_tokens) / 1000:.3f}$ to embed everything&#39;)<br>  return subject_lines<br><br>subject_lines = {}<br>for qa_dct in questions_answers:<br>  subject_lines[qa_dct[&#39;subject&#39;]] = get_lines_per_subject(hebrew_wiki, qa_dct[&#39;subject&#39;], qa_dct[&#39;subject_translation&#39;])</pre><pre>There are 7772 sentences that have the words Australia in them<br>In total it amounts to 1026137 tokens<br>With the current pricing of openAI it will cost 0.041$ to embed everything<br>There are 15908 sentences that have the words University in them<br>In total it amounts to 1904404 tokens<br>With the current pricing of openAI it will cost 0.076$ to embed everything<br>There are 160 sentences that have the words The black plague in them<br>In total it amounts to 19410 tokens<br>With the current pricing of openAI it will cost 0.001$ to embed everything</pre><p>That’s better. We are now ready to populate the vector database:</p><pre>import pinecone<br><br>index_name = &#39;hebrew-wikipedia&#39;<br><br># initialize connection to pinecone (get API key at app.pinecone.io)<br>pinecone.init(<br>    api_key=pinecone_key,<br>    environment=&quot;us-east1-gcp&quot;<br>)<br><br># check if index already exists (it shouldn&#39;t if this is first time)<br>if index_name not in pinecone.list_indexes():<br>    # if does not exist, create index<br>    pinecone.create_index(<br>        index_name,<br>        dimension=len(res[&#39;data&#39;][0][&#39;embedding&#39;]),<br>        metric=&#39;cosine&#39;,<br>        metadata_config={&#39;indexed&#39;: [&#39;contains_keywords&#39;]}<br>    )<br># connect to index<br>index = pinecone.Index(index_name)<br># view index stats<br>index.describe_index_stats()</pre><p>We generated a new Pinecone DB index (if it didn’t exist yet). Let’s populate it with our data:</p><pre>from tqdm.auto import tqdm<br>import datetime<br>from time import sleep<br><br>embed_model = &quot;text-embedding-ada-002&quot;<br>batch_size = 32  # how many embeddings we create and insert at once<br><br>def insert_into_pinecone(lines, subject):<br>  print(f&#39;Inserting {len(lines)} of subject: {subject} into pinecone&#39;)<br>  for i in tqdm(range(0, len(lines), batch_size)):<br>      # find end of batch<br>      i_end = min(len(lines), i+batch_size)<br>      meta_batch = lines[i:i_end]<br>      ids_batch = [str(x[0]) for x in meta_batch]<br>      texts = [x[1] for x in meta_batch]<br>      try:<br>          res = openai.Embedding.create(input=texts, engine=embed_model)<br>      except:<br>          done = False<br>          while not done:<br>              sleep(5)<br>              try:<br>                  res = openai.Embedding.create(input=texts, engine=embed_model)<br>                  done = True<br>              except:<br>                  pass<br>      embeds = [record[&#39;embedding&#39;] for record in res[&#39;data&#39;]]<br>      # cleanup metadata<br>      meta_batch = [{<br>          &#39;contains_keywords&#39;: subject,<br>          &#39;text&#39;: x,<br>      } for x in texts]<br>      to_upsert = list(zip(ids_batch, embeds, meta_batch))<br>      # upsert to Pinecone<br>      index.upsert(vectors=to_upsert)<br><br>for subject, lines in subject_lines.items():<br>  insert_into_pinecone(lines, subject)</pre><p>Great! We have a populated vector DB. Let’s get the top-2 contexts for the first question (Which city in Victoria is considered the sporting capital of Australia?):</p><pre>res = openai.Embedding.create(<br>    input=[questions_answers[0][&#39;question&#39;]],<br>    engine=embed_model<br>)<br><br># retrieve from Pinecone<br>xq = res[&#39;data&#39;][0][&#39;embedding&#39;]<br><br># get relevant contexts (including the questions)<br>res = index.query(xq, top_k=2, include_metadata=True)<br><br>res</pre><pre>{&#39;matches&#39;: [{&#39;id&#39;: &#39;2642286&#39;,<br>              &#39;metadata&#39;: {&#39;contains_keywords&#39;: &#39;אוסטרליה&#39;,<br>                           &#39;text&#39;: &#39;העיר שוכנת לחופה הצפון-מערבי של &#39;<br>                                   &#39;אוסטרליה.\n&#39;},<br>              &#39;score&#39;: 0.899194837,<br>              &#39;values&#39;: []},<br>             {&#39;id&#39;: &#39;2282112&#39;,<br>              &#39;metadata&#39;: {&#39;contains_keywords&#39;: &#39;אוסטרליה&#39;,<br>                           &#39;text&#39;: &#39;אוסטרלאסיה היה השם של נבחרת הספורטאים &#39;<br>                                   &#39;מאוסטרליה וניו זילנד שהשתתפה באולימפיאדת &#39;<br>                                   &#39;לונדון (1908) ובאולימפיאדת סטוקהולם &#39;<br>                                   &#39;(1912).\n&#39;},<br>              &#39;score&#39;: 0.893916488,<br>              &#39;values&#39;: []}],<br> &#39;namespace&#39;: &#39;&#39;}</pre><p>We see that the top 2 lines in our vector DB (in terms of cosine similarity to the question’s embedding) are not so helpful. The first line is a very general sentence about Australia and the second indeed talks about sports in Australia, but there is no mention of Melbourne which is the answer we are expecting.</p><p>But still, let’s challenge our model and see how it fares now that it has access to additional information in Hebrew. Notice that we instructed our model to answer “I don’t know” if the answer cannot be deduced from the given context.</p><pre>limit = 3000<br><br>def retrieve(query, filter_subjects=None, add_dont_know=False):<br>    res = openai.Embedding.create(<br>        input=[query],<br>        engine=embed_model<br>    )<br><br>    # retrieve from Pinecone<br>    xq = res[&#39;data&#39;][0][&#39;embedding&#39;]<br><br>    # get relevant contexts<br>    res = index.query(xq,<br>                      {&#39;contains_keywords&#39;: {&#39;$in&#39;: filter_subjects}}<br>                      if filter_subjects is not None else None,<br>                      top_k=100,<br>                      include_metadata=True)<br>    contexts = [<br>        x[&#39;metadata&#39;][&#39;text&#39;] for x in res[&#39;matches&#39;]<br>    ]<br><br>    # build our prompt with the retrieved contexts included<br>    idk_str = &#39;&#39;&#39;. If the question cannot be answered using the<br>    information provided answer with &quot;I don&#39;t know&quot;&#39;&#39;&#39; if add_dont_know else &#39;&#39;<br>    prompt_start = (<br>        &quot;Answer the question based on the context below.&quot; + idk_str + &quot;\n\n&quot; +<br>        &quot;Context:\n&quot;<br>    )<br>    prompt_end = (<br>        f&quot;\n\nQuestion: {query}\nAnswer:&quot;<br>    )<br>    # append contexts until hitting limit<br>    for i in range(1, len(contexts)):<br>        if len(&quot;\n\n---\n\n&quot;.join(contexts[:i])) &gt;= limit:<br>            prompt = (<br>                prompt_start +<br>                &quot;\n\n---\n\n&quot;.join(contexts[:i-1]) +<br>                prompt_end<br>            )<br>            break<br>        elif i == len(contexts)-1:<br>            prompt = (<br>                prompt_start +<br>                &quot;\n\n---\n\n&quot;.join(contexts) +<br>                prompt_end<br>            )<br>    return prompt<br><br><br>original_subjects = [&#39;אוסטרליה&#39;, &#39;אוניברסיטה&#39;, &#39;המוות השחור&#39;]<br><br>for i, qa_dct in enumerate(questions_answers):<br>  print(f&#39;####################&#39;)<br>  print(f&#39;Question #{i + 1} with context:&#39;)<br>  query_with_contexts = retrieve(qa_dct[&#39;question&#39;],<br>                                 filter_subjects=original_subjects,<br>                                 add_dont_know=True)<br>  print(f&#39;Length of query with contexts: {len(query_with_contexts)}&#39;)<br>  print_question_answer(qa_dct[&#39;question&#39;],<br>                        query_with_contexts,<br>                        qa_dct[&#39;answer&#39;],<br>                        qa_dct[&#39;question_translation&#39;],<br>                        qa_dct[&#39;answer_translation&#39;],<br>                        )</pre><pre>####################<br>Question #1 with context:<br>Length of query with contexts: 3207<br>Question: איזו עיר בויקטוריה נחשבת לבירת הספורט של אוסטרליה?<br>Question (translation): Which city in Victoria is considered the sporting capital of Australia?<br>Answer: קנברה<br>Answer (translation): Canberra<br>Expected Answer: מלבורן<br>Expected Answer (translation): Melbourne<br>####################<br>Question #2 with context:<br>Length of query with contexts: 3179<br>Question: איזה נהר ממוקם בקרבת אוניברסיטת הארווארד?<br>Question (translation): Which river is located in the vicinity of Harvard University?<br>Answer: I don&#39;t know<br>Answer (translation): I don&#39;t know<br>Expected Answer: נהר צ&#39;ארלס<br>Expected Answer (translation): Charles River<br>####################<br>Question #3 with context:<br>Length of query with contexts: 3138<br>Question: כמה אנשים ברחבי העולם נספו בעקבות המוות השחור?<br>Question (translation): How many people in the world died from the black plague?<br>Answer: I don&#39;t know.<br>Answer (translation): I don&#39;t know.<br>Expected Answer: בין 75 ל200 מיליון איש<br>Expected Answer (translation): Between 75 and 200 million</pre><p>OK, getting a bit better. The first answer is still wrong. In the second and third answers, we got “I don’t know” which I perceive as a major improvement over getting wrong answers. But still, we want to give current answers, how can we improve on this..?</p><p>Let’s add some additional subjects to the vector DB, maybe it didn’t have enough relevant information from which to pull a relevant context.</p><pre>additional_subjects = [<br>    (&#39;מלבורן&#39;, &#39;Melbourne&#39;),<br>    (&#39;סידני&#39;, &#39;Sydney&#39;),<br>    (&#39;אדלייד&#39;, &#39;Adelaide&#39;),<br>    (&#39;הרווארד&#39;, &#39;Harvard&#39;),<br>    (&quot;מסצ&#39;וסטס&quot;, &#39;Massachusetts&#39;),<br>    (&quot;צ&#39;ארלס&quot;, &#39;Charles&#39;),<br>    (&#39;בוסטון&#39;, &#39;Boston&#39;),<br>    (&#39;מגפה&#39;, &#39;plague&#39;),<br>    (&#39;האבעבועות השחורות&#39;, &#39;smallpox&#39;),<br>    (&#39;אבעבועות שחורות&#39;, &#39;smallpox&#39;),<br>    (&#39;מונגוליה&#39;, &#39;mongolia&#39;)<br>]<br><br>additional_subject_lines = {}<br>for subject, subject_translation in additional_subjects:<br>  additional_subject_lines[subject] = get_lines_per_subject(hebrew_wiki, subject, subject_translation)<br><br>for subject, lines in additional_subject_lines.items():<br>  insert_into_pinecone(lines, subject)</pre><pre>There are 948 sentences that have the words Melbourne in them<br>In total it amounts to 119669 tokens<br>With the current pricing of openAI it will cost 0.005$ to embed everything<br>There are 2812 sentences that have the words Sydney in them<br>In total it amounts to 347608 tokens<br>With the current pricing of openAI it will cost 0.014$ to embed everything<br>There are 213 sentences that have the words Adelaide in them<br>In total it amounts to 24480 tokens<br>With the current pricing of openAI it will cost 0.001$ to embed everything<br>There are 1809 sentences that have the words Harvard in them<br>In total it amounts to 228918 tokens<br>With the current pricing of openAI it will cost 0.009$ to embed everything<br>There are 1476 sentences that have the words Massachusetts in them<br>In total it amounts to 179227 tokens<br>With the current pricing of openAI it will cost 0.007$ to embed everything<br>There are 5025 sentences that have the words Charles in them<br>In total it amounts to 647501 tokens<br>With the current pricing of openAI it will cost 0.026$ to embed everything<br>There are 2912 sentences that have the words Boston in them<br>In total it amounts to 361337 tokens<br>With the current pricing of openAI it will cost 0.014$ to embed everything<br>There are 1007 sentences that have the words plague in them<br>In total it amounts to 116961 tokens<br>With the current pricing of openAI it will cost 0.005$ to embed everything<br>There are 39 sentences that have the words smallpox in them<br>In total it amounts to 5417 tokens<br>With the current pricing of openAI it will cost 0.000$ to embed everything<br>There are 135 sentences that have the words smallpox in them<br>In total it amounts to 15815 tokens<br>With the current pricing of openAI it will cost 0.001$ to embed everything<br>There are 742 sentences that have the words mongolia in them<br>In total it amounts to 90607 tokens<br>With the current pricing of openAI it will cost 0.004$ to embed everything</pre><p>Great, we added richer context (you might even say we are cheating a bit — notice that we added all sentences that contain the words “Melbourne” and “Charles” amongst others, which are the answers to our questions. But it’s just a demonstration!)</p><pre>for i, qa_dct in enumerate(questions_answers):<br>  print(f&#39;####################&#39;)<br>  print(f&#39;Question #{i + 1} with context:&#39;)<br>  query_with_contexts = retrieve(qa_dct[&#39;question&#39;], add_dont_know=Tue)<br>  print(f&#39;Length of query with contexts: {len(query_with_contexts)}&#39;)<br>  print_question_answer(qa_dct[&#39;question&#39;],<br>                        query_with_contexts,<br>                        qa_dct[&#39;answer&#39;],<br>                        qa_dct[&#39;question_translation&#39;],<br>                        qa_dct[&#39;answer_translation&#39;],<br>                        )</pre><pre>####################<br>Question #1 with context:<br>Length of query with contexts: 3158<br>Question: איזו עיר בויקטוריה נחשבת לבירת הספורט של אוסטרליה?<br>Question (translation): Which city in Victoria is considered the sporting capital of Australia?<br>Answer: מלבורן<br>Answer (translation): Melbourne<br>Expected Answer: מלבורן<br>Expected Answer (translation): Melbourne<br>####################<br>Question #2 with context:<br>Length of query with contexts: 3106<br>Question: איזה נהר ממוקם בקרבת אוניברסיטת הארווארד?<br>Question (translation): Which river is located in the vicinity of Harvard University?<br>Answer: נהר צ&#39;ארלס<br>Answer (translation): Charles River<br>Expected Answer: נהר צ&#39;ארלס<br>Expected Answer (translation): Charles River<br>####################<br>Question #3 with context:<br>Length of query with contexts: 3203<br>Question: כמה אנשים ברחבי העולם נספו בעקבות המוות השחור?<br>Question (translation): How many people in the world died from the black plague?<br>Answer: לפי הערכות שונות, כ-35 מיליון בני אדם בסין לבדה, ובין 20 ל-25 מיליון בני אדם באירופה.<br>Answer (translation): According to various estimates, about 35 million people in China alone, and between 20 and 25 million people in Europe.<br>Expected Answer: בין 75 ל200 מיליון איש<br>Expected Answer (translation): Between 75 and 200 million</pre><p>Nice! We have our answers! Although the third answer is still not 100% correct, but way better than “I don’t know” and way better than the initial hallucination.</p><h3>Part 5. Conclusions</h3><p>In this article, we have seen how to automatically provide relevant context to an LLM using a vector database in order to improve the model outputs. In this specific case, it might not have been necessary because the performance of ChatGPT (which is available as an API) on Hebrew has improved tremendously and it is able to answer these questions very well out-of-the-box.</p><p>Yet still, the point of this article was to introduce this concept, which can be generalized for other purposes such as:</p><ul><li>Using an LLM to analyze proprietary data which is not available online</li><li>Adding a form of explainability to the model, by appending the context which was used to the output of the user (to prove the credibility of the answer).</li></ul><p>I hope you enjoyed this article and were motivated to try it for yourself. See you next time!</p><p>Elad</p><h3>References</h3><ul><li>Hebrew Wikipedia dump — <a href="https://u.cs.biu.ac.il/~yogo/hebwiki/">https://u.cs.biu.ac.il/~yogo/hebwiki/</a></li><li>SQuAD dataset — <a href="https://rajpurkar.github.io/SQuAD-explorer/">https://rajpurkar.github.io/SQuAD-explorer/</a></li><li>Video tutorial by James Briggs — <a href="https://youtu.be/dRUIGgNBvVk">https://youtu.be/dRUIGgNBvVk</a></li><li>OpenAI API — <a href="https://openai.com/blog/openai-api">https://openai.com/blog/openai-api</a></li><li>Pinecone DB — <a href="https://www.pinecone.io/">https://www.pinecone.io/</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=20f5fb5a2f36" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/improving-hebrew-q-a-models-via-prompting-20f5fb5a2f36">Improving Hebrew Q&amp;A Models via Prompting</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[A Sentence Is Greater than the Sum of Its Words]]></title>
            <link>https://medium.com/data-science/a-sentence-is-greater-than-the-sum-of-its-words-56ce3967fcc6?source=rss-d2d1ff8f0490------2</link>
            <guid isPermaLink="false">https://medium.com/p/56ce3967fcc6</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[transformers]]></category>
            <category><![CDATA[deep-dives]]></category>
            <category><![CDATA[naturallanguageprocessing]]></category>
            <dc:creator><![CDATA[Elad Rapaport]]></dc:creator>
            <pubDate>Fri, 02 Dec 2022 07:02:11 GMT</pubDate>
            <atom:updated>2022-12-03T11:37:12.918Z</atom:updated>
            <content:encoded><![CDATA[<h4>Sentence Embeddings 101</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DbLnH4kLjofQL06rv7P5dA.jpeg" /><figcaption>Image by <a href="https://pixabay.com/users/ri_ya-12911237/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=6709160">Ri Butov</a> from <a href="https://pixabay.com//?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=6709160">Pixabay</a></figcaption></figure><p>In this article, I will present the topic of sentence embeddings. We will learn what sentence embeddings are and how they can be helpful. We will review some basic (and even naive) methods for creating sentence embeddings and also some recent advances in the field. We will end with an experiment I conducted on measuring document visualization quality when using different sentence embedding techniques. So without further ado, let’s jump right in!</p><p>The article will be organized as follows:</p><p>Part 1. What are Sentence Embeddings?<br>Part 2. An Example Application<br>Part 3. Some Basic Methods<br>Part 4. SentenceBERT [1] — The New Kid on the Block<br>Part 5. SimCSE [2] — Enter Unsupervised Sentence Embeddings<br>Part 6. Experiment — Measuring Document Visualization Quality — Kaggle News Dataset [3]</p><ul><li>The code I used for the experiment is available here — <a href="https://github.com/erap129/DocumentVisualizations">https://github.com/erap129/DocumentVisualizations</a></li></ul><h3>Part 1 — What are Sentence Embeddings?</h3><p>Embeddings in machine learning are vector representations of entities in the world, so sentence embeddings are simply vector representations of sentences. Usually we say that they are <em>distributed</em> vector representations, and this means that we don’t have a 1:1 mapping from traits in the data to elements in the vector (for example — a single vector element representing the “happiness” of a sentence), but rather these traits are distributed amongst the different elements of the vector in ways that we cannot pre-define.</p><p>For example, in figure 1 we can see a toy example of some sentence embeddings being generated. We can see that the embeddings manage to capture the semantic similarity between the first two (questions about food) and last two (schedule-related facts) sentences while maintaining a difference between these two groups.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LUWxFvVegudPJQtSKMXTgQ.png" /><figcaption>Fig. 1: sentence embeddings — toy example</figcaption></figure><h3>Part 2— An Example Application</h3><p>So, what are these sentence embeddings good for, anyway? Let’s see a hypothetical example application that I thought of (this is just an example, I’m sure you will be able to conjure more of these ideas once you see it).</p><p>In figure 2 we can see an example application where a user writes a piece of code (insertion sort in Python) and wants to find similar code snippets (maybe to find better implementations, or to find the same function in other languages). The user takes this code and uses sentence embedding methods to transform the code snippet into a distributed vector representation. Then, the user queries a database of other code snippet embeddings and using K nearest neighbors with cosine similarity finds the most similar snippets to the input snippet.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_pcrjsOiwxezFPjViIAPfQ.png" /><figcaption>Fig. 2: an example application</figcaption></figure><h3>Part 3— Some Basic Methods</h3><p>The most basic methods in this field are counting-based. The simplest example of counting-based methods is shown in the top of figure 3, where the count of each word in a sentence is an entry in the vector representation of the sentence. This is a <em>non-distributed</em> vector representation, as a single trait of a sentence (a specific word count) can be mapped directly to a single entry in the representation vector. Other methods, such as TF-IDF have evolved this by normalizing these values by the frequency of each word in the entire dataset, but they are still counting-based and don’t take into account the ordering of the words in the sentence (which can be a major source of data loss).</p><p>A second method is shown in the bottom of figure 3, where certain word embedding methods are used out-of-the-box, and the representation for the sentence is taken as an average of these word embeddings. The word embeddings can be generated by any method (GloVe [5], word2vec, BERT, etc) but we will see later that there is a drawback in simply taking the average of these vectors without tuning them first to better fit the task of sentence embeddings.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xGh5088wU7LbXobutLDJMg.png" /><figcaption>Fig. 3: some basic sentence embedding methods</figcaption></figure><h3>Part 4— SentenceBERT — The New Kid on the Block</h3><p>A major advancement in the field of sentence embeddings occurred in 2019 when the Sentence-BERT paper was released by Nils Reimers and Iryna Gurevych. They introduced a new method to fine-tune the well-known BERT model to produce embeddings that will be well comparable using cosine similarity.</p><p>The original BERT [4] paper dealt with the problem of semantic textual similarity, where the goal is to evaluate how similar a pair of sentences are (example shown in figure 4), and even reached new state-of-the-art results on a specific benchmark.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/895/1*8UyPzX_AlZQR7FppQtp1dw.png" /><figcaption>Fig 4. an example of a semantic textual similarity (STS) task</figcaption></figure><p>The problem with BERT’s approach was its incredible inefficiency. BERT solved this task by encoding the tokens of two sentences at once (separated by a special [SEP]token) and outputting the level of similarity at the end, as shown in figure 5, which is taken from the original BERT paper. To find the most similar pair in a dataset of 10K sentences would take BERT ~65 hours of inference time. Thus, the goal of SBERT was to create embeddings that can be compared simply using cosine similarity, while still yielding state-of-the-art results on benchmark STS tasks.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/503/1*b8GCVkwsSiDVM2NwBzix7g.png" /><figcaption>Fig. 5: BERT’s approach to semantic textual similarity tasks (figure taken from original paper)</figcaption></figure><p>I will focus here on one of the solutions proposed in the SBERT paper, which is to use siamese networks on labeled sentence pairs from a natural language inference (NLI) task dataset. The sentence pairs in this dataset were labeled with one of the following options: entailment, contradiction or neutral.</p><p>The fine-tuning task introduced by SBERT and applied to the original BERT pre-trained model was to take each labeled sentence-pair and use siamese networks to generate a vector containing both BERT representations (the concatenation of v, u, |u-v|, where u and v are the outputs of BERT). A feedforward network with a softmax function is tacked on top of this structure and it’s output determines whether the sentence pair is an entailment, a contradiction or neutral. The process described above is depicted in figure 6.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/467/1*C-8YO-y6_HXGLv8tM5uRfQ.png" /><figcaption>Fig. 6: SBERT fine-tuning procedure for NLI data</figcaption></figure><p>This simple fine-tuning task created an improved BERT model which generates sentence representations that can be compared using simple cosine similarity! In table 1 (taken from the original SBERT paper), the authors show that they outperform naive methods (average GloVe embeddings, average BERT embeddings) by a large margin, and also outperform other sentence embedding methods which we have not covered here.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1007/1*2IYAhPRteJ_pPKR46lSytg.png" /><figcaption>Table 1. Results from the original SBERT paper</figcaption></figure><p>But what if our data doesn’t have labeled sentence pairs which we can use to train SBERT? That’s where SimCSE comes in…</p><h3>Part 5— SimCSE — Enter Unsupervised Sentence Embeddings</h3><p>SimCSE, written by Tianyu Gao, Xingcheng Yao and Danqi Chen introduces a novel way of fine-tuning language models for the purpose of creating sentence embeddings. In this paper the authors use contrastive learning, where the positive example is the sentence itself passed through a different dropout mask (more on this later).</p><p><em>Note — The paper also presents a supervised way to create sentence embeddings that uses contrastive learning. I chose to focus on the unsupervised part in this article. Refer to the original paper for full details.</em></p><p>First let’s have a refresher on contrastive learning: the main idea is to pull together close neighbors and push apart non-neighbors. We will be following Mario, Luigi and Yoshi in this example. Mario and Luigi are twins, so we want the embeddings that represent them to be close in vector space. Yoshi is cute, but not very similar to Mario and Luigi so we want his representation to be further away.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/602/1*cg-2FHVh_oTQOY5yUkuZMw.png" /><figcaption>Fig. 7: cartoon example of contrastive learning objective</figcaption></figure><p>But how do we achieve this numerically? Using a loss function of course! In contrastive learning, we generally use a loss function called InfoNCE. Figure 8 is an infographic that will help us understand the InfoNCE loss function, let’s go over it step by step.</p><p>First, in the numerator, we see that we have an exponent of the similarity of h_i and (h_i)^+ (sorry for the bad notation). We see that h_i represents Mario and (h_i)^+ represents Luigi, thus we want the numerator to be large, of course. On the other hand in the denominator we have the sum of similarities of Mario h_i with all of the other elements in the batch (h_j)^+ , this includes Luigi but it also includes other stuff that we don’t want Mario to be similar to, including a plane, a dish and Yoshi of course. We see that the numerator is a part of the denominator and if we make the analogy to a cake — the numerator is a slice of the denominator. This slice should be as large as possible so as to maximize the similarity between Mario and Luigi while minimizing the similarity between Mario and everything else in the batch.</p><p>Notice that there is a negative logarithm function wrapping this whole thing. We see that as this fraction gets closer to 1, the negative logarithm gets smaller and thus by using gradient descent with this objective function we are pushing similar items to have a higher similarity, and dissimilar items to have a lower similarity.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*joPUxeIA0i9zimo9f5gHMA.png" /><figcaption>Fig. 8: InfoNCE loss infographic</figcaption></figure><p>Now we know how contrastive learning works, but which “Mario, Luigi and Yoshi” did SimSCE use when no labeled data of this sort was available? It turns out that for a given sentence they passed it through BERT twice, each time using a different dropout mask (reminder — dropout is a function which zeros out a fraction of the input randomly), thus receiving two slightly different embeddings for a given sentence. For negative examples they simply used all of the other sentences in the mini-batch (given good shuffling — the other sentences should be quite different).</p><p>Figure 9 shows an example of this (contains a figure from the original SimCSE paper). On the left we see that the sentence “Two dogs are running” is passed twice through the network, thus creating a positive pair, while all of the other sentences in the batch (A main surfing on the sea, A kid is on a skateboard) serve as negative pair samples. On the right is just a reminder of how dropout works by masking out inputs randomly.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iQ90AWbHh8YG-NXx3EEFVA.png" /><figcaption>Fig. 9: positive and negative pairs for contrastive learning in SimCSE</figcaption></figure><p>OK, but why should this even work? What is the theoretical basis behind this? Here the authors of SimCSE explain to us about two terms called Alignment and Uniformity.</p><p>In figure 10 we can see the equations for alignment and uniformity, along with our trusty friends Mario, Yoshi and Luigi. Alignment is the expected distance between embeddings of paired instances. The term on the left is the alignment “loss” which we will aim to minimize. We see that it contains the distances between the embeddings of all of the positive pairs (Marios and Luigis) which we will want to minimize. On the right we see the uniformity loss. We have <em>e</em> to the negative power of the distances between all of the negative pairs (Marios and Yoshis), which we will want to maximize.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bZdDQa9Jo0JyDxOjMSX6kA.png" /><figcaption>Fig. 10: alignment and uniformity</figcaption></figure><p>The authors argue that pre-trained models generally have good alignment — that is, pairs of positive samples will be close in the embedding space, but poor uniformity — that is, pairs of negative samples won’t be far enough from each other. Figure 11 is taken from the original SimCSE paper and it shows that they managed to keep a good alignment while improving the uniformity (minimizing the uniformity loss). The red stars represent the SimCSE model and the other symbols are other baselines that were tested.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/619/1*7QnpxZ6QGMZAqqNZqfUyxQ.png" /><figcaption>Fig. 11: SimCSE uniformity and alignment over time</figcaption></figure><p>When we look at the results of SimCSE on STS benchmark tasks in table 2, we see similar results to SBERT that we reviewed earlier, only this time — no labeled data regarding sentence similarity was used! They only fine-tuned on 1 million sentences from Wikipedia. That’s pretty impressive in my opinion.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/947/1*0vOwNKg6qPfao5e5Gu2FWw.png" /><figcaption>Table 2: results for unsupervised SimCSE</figcaption></figure><h3>Part 6— Experiment — Measuring Document Visualization Quality — Kaggle News Dataset</h3><p>That’s it for the theoretical part of this article. I am now going to show an experiment I conducted using the above models regarding the visualization of documents in 2D space.</p><p>The code I used for this experiment is available here — <a href="https://github.com/erap129/DocumentVisualizations">https://github.com/erap129/DocumentVisualizations</a></p><p>The News Category Dataset from Kaggle — <a href="https://www.kaggle.com/datasets/rmisra/news-category-dataset">https://www.kaggle.com/datasets/rmisra/news-category-dataset</a> (released under the Attribution 4.0 International (CC BY 4.0) license) contains news articles belonging to 42 different categories. Each article in the dataset contains the following data fields: category, headline, authors, link, short_description, date. I took only the headline, short_description fields and created a new field which I called long_description. My goal was to create embeddings of the long_description of each news article using several methods, and check which method results in the best 2D visualization of the data. So a few questions need to be asked here.</p><ul><li><em>How did I create a 2D visualization given the sentence embedding of a specific article?</em></li></ul><p>I used the UMAP algorithm. There is a plethora of documentation regarding this great dimensionality reduction algorithm so I won’t go into details about it here. I will just say that I used the default hyper-parameters in all experiments with no tuning whatsoever.</p><ul><li><em>How did I quantitatively compare the qualities of the different visualizations? What does it mean to have a “good” visualization?</em></li></ul><p>The comparison of visualization is done, of course, visually “by eye”. But what if we still want to get a metric which we can hang on to? Maybe we are pitting dozens of algorithms against each other and don’t have time to eyeball them all? I came across this paper — <a href="http://graphics.uni-konstanz.de/publikationen/Sips2009SelectingGoodViews/Sips2009SelectingGoodViews.pdf">Selecting good views of high-dimensional data using class consistency</a> which suggests a measure called <em>distance consistency</em> which is defined as so:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XUL_6mdNV8KDABUou25Gfw.png" /><figcaption>Eq. 1: distance consistency metric</figcaption></figure><p>For each point <em>x’ </em>in the visualized space <em>v(X)</em>, we check whether the closest class’s centroid is different than the class of <em>x</em>, and then divide by <em>N</em> the number of samples to get a number between 0 and 1. As the <em>DSC </em>of the visualization is lower it means that the clusters representing the different groups in the visualization are more prominent, which is good. This measure applies only when the data is labeled, which is our situation with the news dataset.</p><ul><li><em>Which methods yielded which results? Are my findings in line with the SBERT and SimCSE papers?</em></li></ul><p>Let’s go into the experiment results. For simplicity, I used only 5 categories from the Kaggle news dataset: <em>food&amp;drink, worldpost, sports, divorce</em> and <em>style. </em>I compared the following methods for creating visualizations:</p><ul><li>TF-IDF — The counting based method for creating document-representing vectors. Used as a baseline.</li><li>bert_average — Average embedding values of all the tokens in the document, taken from the final layer of a BERT model (not fine-tuned for sentences)</li><li>bert_cls — The embedding of the [CLS] token taken from the final layer of BERT.</li><li>sbert_average — Average embedding values from SBERT (the supervised fine-tuning of BERT for sentences)</li><li>sbert_cls — The embedding of the [CLS] token taken from the final layer of SBERT.</li><li>simcse_average — Average embedding values from SimCSE (the unsupervised fine-tuning of BERT for sentences)</li><li>simcse_cls — The embedding of the [CLS] token taken from the final layer of SimCSE.</li></ul><p>Here are the results for the <em>DSC </em>measure:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2LXEBrmcp0hUC1W09PkKBg.png" /><figcaption>Fig. 12: DSC of generated visualizations, per embedding method</figcaption></figure><p>We see very similar results to the SBERT and SimCSE papers. The non-finetuned BERT has hard competition from the simple TF-IDF baseline. After supervised finetuning (SBERT) the results improve dramatically, and also via unsupervised finetuning (SimCSE). In BERT and SBERT using the average token embeddings as a basis for dimensionality reduction was advantageous over taken the embedding of the [CLS] token, while for SimCSE the opposite is true.</p><p>Let’s view the actual visualization for each method to get a better feel for what has been done here. A quick inspections confirms the DSC metric — methods that got a lower DSC produce more discernable clusters in the visualization:</p><ul><li>TF-IDF</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jKEu5Ti0iY_9zfQncsf9Ug.png" /><figcaption>Fig. 13: tfidf embeddings visualization by UMAP</figcaption></figure><ul><li>bert_average</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*suw_D4pVLm6Gjb6lrT3eKA.png" /><figcaption>Fig. 14: bert_average embeddings visualization by UMAP</figcaption></figure><ul><li>bert_cls</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YNN6GRzgIRhvwFj_mnK_RA.png" /><figcaption>Fig. 15: bert_cls embeddings visualization by UMAP</figcaption></figure><ul><li>sbert_average</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vR0HwSYR8jyESjfx0kY9eQ.png" /><figcaption>Fig. 16: sbert_average embeddings visualization by UMAP</figcaption></figure><ul><li>sbert_cls</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dTdm8K7TrPTszOhi-5ZaJg.png" /><figcaption>Fig. 17: sbert_cls embeddings visualization by UMAP</figcaption></figure><ul><li>simcse_average</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*39N6psT3SMoII9RlFkqhIg.png" /><figcaption>Fig. 18: simcse_average embeddings visualization by UMAP</figcaption></figure><ul><li>simcse_cls</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OVyS93rZl9X5e2n4wioC5A.png" /><figcaption>Fig. 19: simcse_cls embeddings visualization by UMAP</figcaption></figure><h3>Conclusion</h3><p>In this article we:</p><ul><li>learned what sentence embeddings are</li><li>learned about two state-of-the-art methods for creating sentence embeddings by finetuning BERT (both supervised and unsupervised)</li><li>saw that the results from the above papers repeat also in a visualization quality experiment</li></ul><p>For future work I think it would be interesting to try out SimCSE on other types of textual data and not just natural language. For example, to write a model that finds similar code snippets as I suggested in part 2 of this article. The fact that SimCSE does not need supervised data to fine-tune for sentences will allow training on various kinds of data.</p><p>I hope you enjoyed reading and learned something new. Thanks for reading!</p><h3>References</h3><p>[1] — SentenceBERT paper — <a href="https://arxiv.org/abs/1908.10084">https://arxiv.org/abs/1908.10084</a><br>[2] — SimCSE paper — <a href="https://arxiv.org/abs/2104.08821">https://arxiv.org/abs/2104.08821</a><br>[3] — Kaggle news categories dataset — <a href="https://www.kaggle.com/datasets/rmisra/news-category-dataset">https://www.kaggle.com/datasets/rmisra/news-category-dataset</a><br>[4] — BERT paper — <a href="https://arxiv.org/abs/1810.04805">https://arxiv.org/abs/1810.04805</a><br>[5] — GloVe embeddings — <a href="https://nlp.stanford.edu/projects/glove/">https://nlp.stanford.edu/projects/glove/</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=56ce3967fcc6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/a-sentence-is-greater-than-the-sum-of-its-words-56ce3967fcc6">A Sentence Is Greater than the Sum of Its Words</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[How I Built a Website Integrating Medium Articles — Using Jekyll and GitHub Pages]]></title>
            <link>https://medium.com/better-programming/how-i-built-a-website-integrated-with-medium-using-jekyll-and-github-pages-c8f505e3b9e3?source=rss-d2d1ff8f0490------2</link>
            <guid isPermaLink="false">https://medium.com/p/c8f505e3b9e3</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[medium]]></category>
            <category><![CDATA[github-pages]]></category>
            <dc:creator><![CDATA[Elad Rapaport]]></dc:creator>
            <pubDate>Fri, 04 Nov 2022 15:43:01 GMT</pubDate>
            <atom:updated>2022-11-04T15:47:45.955Z</atom:updated>
            <content:encoded><![CDATA[<h4>A step-by-step guide to help you save time and effort</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uPwyuzwql9_jWpdFVBvCIQ.jpeg" /><figcaption>Photo by Ketut Subiyanto: <a href="https://www.pexels.com/photo/faceless-person-using-touchpad-on-laptop-4126724/">https://www.pexels.com/photo/faceless-person-using-touchpad-on-laptop-4126724/</a></figcaption></figure><p>In this article, I will share the process of creating my personal website and portfolio using <a href="https://jekyllrb.com/">Jekyll</a>. Jekyll is a <a href="https://guides.rubygems.org/what-is-a-gem/">Ruby Gem</a> used to easily create static websites whose content is predetermined at build time and not dependent on any live user input or database connection.</p><p>As I am not a web designer and didn’t want to spend time designing in CSS and HTML, I searched for a nice and impressive template and stumbled upon Beautiful Jekyll (<a href="https://beautifuljekyll.com/">https://beautifuljekyll.com/</a>) by Dean Atalli, which I will be customizing in this post. This is my site — <a href="https://erap129.github.io/index">https://erap129.github.io</a>, which is what we will learn to create, and this is the site’s repository — <a href="https://github.com/erap129/erap129.github.io">https://github.com/erap129/erap129.github.io</a>.</p><p>This article will be organized as follows:</p><ol><li>Requirements — what I need/want from my site</li><li>Setups and installations</li><li>Building the website</li><li>Deploying the website</li></ol><h3>Part 1 — Requirements</h3><p>Before I started working, I defined my main goals for this website. It should:</p><ul><li>Give general information about me to potential and existing clients/collaborators/employers, such as my interests, projects, and hobbies.</li><li>Contain a page containing previews of all my articles on Medium and a link to them on the Medium platform. This page should not be built manually, as I am planning to keep writing on Medium, and I don’t want to copy and paste my posts each time to my personal website. Furthermore, it should be updated automatically so I won’t have to update my site whenever I post on Medium.</li><li>Contain additional static pages — such as my scientific publications, public code contributions, and maybe more.</li></ul><h3>Part 2 — Setups and Installations</h3><p>It is important to say — Beautiful-Jekyll is an awesome template. If the site structure defined by this template is right for you and you don’t need integration with Medium — just skip my article and follow the <a href="https://github.com/daattali/beautiful-jekyll">official instructions</a>.</p><p>I really liked the design of this template, but to create an integration with Medium, I needed to do quite a bit of customization. To this end, I needed the ability to deploy the site locally, which is the purpose of the following installations:</p><p>This document — <a href="https://docs.github.com/en/pages/setting-up-a-github-pages-site-with-jekyll/creating-a-github-pages-site-with-jekyll">https://docs.github.com/en/pages/setting-up-a-github-pages-site-with-jekyll/creating-a-github-pages-site-with-jekyll</a> provides an excellent explanation of how to get your environment set up for work with Jekyll and Github Pages, but I will go over the main steps here as well. I will assume that you are using Ubuntu Linux (if this is not the case — follow the instructions in the link above as per your OS).</p><ul><li>First of all, install Ruby — sudo apt-get install ruby-full</li><li>Now install Bundler — sudo gem install bundler</li><li>Fork Beautiful-Jekyll — The steps are written here -<a href="https://github.com/daattali/beautiful-jekyll#readme">https://github.com/daattali/beautiful-jekyll#readme</a>. They are:<br>1. Fork the project as &lt;yourusername&gt;.github.io<br>2. Clone the project to your PC</li><li>To run the website locally, navigate to the local repository folder in the terminal and run bundle install. This will install the required Ruby software used to run and serve Jekyll. Now each time you wish to serve the website locally run bundle exec jekyll serve. Congrats! The website will be available on <a href="http://localhost:4000/">http://localhost:4000</a></li></ul><h3>Part 3 — Building the website</h3><p>These are the things I will do to customize the Jekyll template to my needs:</p><ol><li>Create the “Blog Posts” page, which will be integrated into Medium</li><li>Create additional markdown-based pages: Home and Publications</li></ol><p>Let’s start!</p><h4>Create the “Blog Posts” page</h4><p>The first question is — how do we get information from Medium? The answer is — RSS! Medium has an RSS feed per author, which allows me to get all of my articles’ information using a simple URL (for me, it’s — <a href="https://medium.com/feed/@erap129">https://medium.com/feed/@erap129</a>).</p><p>I really liked how posts are shown in <a href="https://beautifuljekyll.com/">Beautiful Jekyll</a>, but in my case, I don’t want each preview linking to an internal post but rather to the original post on Medium.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/787/1*LQVxtkaa9bhkVHqIHMWHFQ.png" /><figcaption>Original post formal for Beautiful Jekyll</figcaption></figure><p>To generate content dynamically in Jekyll, we will need to create a <a href="https://jekyllrb.com/docs/plugins/generators/">generator plugin</a>. Plugins in Jekyll are bits of Ruby code that run when you build your website and perform various functions. Our generator will collect all of my Medium posts’ information and store it in a collection we will use in our site’s HTML code.</p><p>Let’s create a folder named _plugins in the main directory of our repo, and create a file called jekyll-display-medium-posts-json.rb with the following content:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/4f276e7184576d8b4723f68f9c59adb1/href">https://medium.com/media/4f276e7184576d8b4723f68f9c59adb1/href</a></iframe><p>This plugin is an extension/modification of the plugin presented in the post by <a href="https://medium.com/u/c040a0cea7ab">James Hamman</a> — <a href="https://medium.com/@jameshamann/displaying-medium-posts-on-your-jekyll-website-7eef230309e4">https://medium.com/@jameshamann/displaying-medium-posts-on-your-jekyll-website-7eef230309e4</a>.</p><p>Let’s go over the code:</p><ul><li>In line 10, we create a new collection called medium_posts_json. We can use this name throughout the site’s code to access our posts.</li><li>In line 12, we call the API of <a href="https://rss2json.com/#rss_url=https%3A%2F%2Fgithub.com%2Frails%2Frails%2Freleases.atom">rss2json</a>. The JSON format gives us better control over the content we receive from Medium, and thus we use this API to convert Medium’s XML format to JSON.</li><li>In lines 18–33, we collect the information that we will be using in our “Blog Posts” page — the articles’ title, image, link, and date (we also collect the categories but don’t use them)</li><li>Notice that we are using the nokogiri Ruby gem to traverse HTML content in line 31. Why? Because the description of each post in the RSS feed is raw HTML, we here want to create a short excerpt of the posts. To skip the headers and images — we extract only the paragraphs (which are in p tags in HTML).</li></ul><p>It is important to add the following snippet to _config.yaml. Otherwise, the collection created by the generator won’t be available to use in the site:</p><pre>collections:</pre><pre>- medium_posts_json</pre><p>Now, we need to create a layout that will use this information and transform it into HTML code rendered by the browser. We will create a new file called blog.html in the _layouts directory with the following content:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/608d6931c2cd565275772c73171d6326/href">https://medium.com/media/608d6931c2cd565275772c73171d6326/href</a></iframe><p>This layout is basically a stripped-down version of the original layout in Beautiful-Jekyll — <a href="https://github.com/daattali/beautiful-jekyll/blob/master/_layouts/home.html">https://github.com/daattali/beautiful-jekyll/blob/master/_layouts/home.html</a>. The only changes I made were to remove irrelevant content and make the layout use the collection I created earlier — site.medium_posts_json.</p><ul><li>Notice in line 5 that we iterate over the collection we made using our plugin and add the required HTML code for each element. Jekyll uses a syntax called Liquid to generate this content. You can read more about Liquid here — <a href="https://shopify.github.io/liquid/">https://shopify.github.io/liquid/</a>.</li></ul><p>Now, all we need to do is to create a new file called blogposts.md in the main directory, and have it use the new layout we created — blog.</p><pre>---</pre><pre>layout: blog</pre><pre>title: Blog Posts</pre><pre>subtitle: This is what keeps my mind busy</pre><pre>---</pre><p>Also, edit the navbar-links section in _config.yaml to include the new blog posts page.</p><p>Great! If I now serve my website locally using bundle exec jekyll serve, I should be able to access my site in <a href="http://localhost:4000/">http://localhost:4000</a> and see the changes that I made.</p><h4>2. Create additional markdown-based pages</h4><p>Now, because we added a Blog Posts page, we no longer need the homepage to list local blog content. Additionally, we would like the home page to be easily editable using Markdown rather than HTML.</p><p>Thus, we will remove index.html and replace it with index.md. I added personal information about myself on this page, but you can do whatever you desire. I also added a publications.md page in which I list my scientific publications. The navbar-links section in my _config.yaml looks like this:</p><pre>navbar-links:</pre><pre>Home: &quot;index&quot;</pre><pre>Blog Posts: &quot;blogposts&quot;</pre><pre>Publications: &quot;publications&quot;</pre><h3>Part 4 — Deploying the Website</h3><p>Once we reached a stage where we run bundle exec jekyll serve and like what we see, we are ready to deploy.</p><p>We want our site to deploy in two cases:</p><ol><li>When we push new/updated content into the repository’s master branch</li><li>Every time a new post on Medium is added, we wish to integrate into the site.</li></ol><p>1 will be easy, and for 2, we will make this a daily process. This means that each day at a set time, the build process for the website will be built, and if I added new content to my Medium profile, it will get pulled to the website.</p><p>We can’t rely on the default GitHub page deployment actions for this because we are using custom plugins, which GitHub pages do not officially support. Plus, we want the build process to be timely and not just based on a push to the master branch. To this end, we will use this custom GitHub Action from the marketplace, called Jekyll Actions — <a href="https://github.com/marketplace/actions/jekyll-actions">https://github.com/marketplace/actions/jekyll-actions</a>.</p><p>The link above has a pretty useful tutorial, but I will still take you through the steps needed to activate this action. From the site’s root directory, I created the following file: .github/workflows/github-pages.yml with the following content:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/4c8fd1e4a26916ce211f2570a08005a8/href">https://medium.com/media/4c8fd1e4a26916ce211f2570a08005a8/href</a></iframe><p>Let’s go over what we see here (the interesting bits)</p><ul><li>In lines 4–8, we define when the site deployment will occur — on a push event to the master branch and every day at midnight. I’m doing this to edit my website manually if I want, but if I publish new content on Medium, I won’t necessarily have to edit it.</li><li>Notice that in lines 23 and 26, I refer to a secret called JEKYLL_PAT. To set this up, I created a GitHub personal access token — <a href="https://github.com/settings/tokens">https://github.com/settings/tokens</a> with full repo access and added it as a secret to my project settings (Repo page -&gt; settings -&gt; secrets -&gt; actions).</li><li>The rest of the code was taken from the original GitHub action and is explained here thoroughly — <a href="https://github.com/marketplace/actions/jekyll-actions">https://github.com/marketplace/actions/jekyll-actions</a>.</li></ul><p>Another important step is to set the deployment branch to gh-pages (the branch that the static content is being pushed to) so GitHub will know to host the files from this branch. I want to give a special thanks to Christian from StackOverflow, who helped me solve an issue regarding this — <a href="https://stackoverflow.com/questions/74244454/helaili-jekyll-action-does-not-update-the-browser-with-gh-pages-branch-content">https://stackoverflow.com/questions/74244454/helaili-jekyll-action-does-not-update-the-browser-with-gh-pages-branch-content</a>. This is done through the repository settings like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XexLYkX3QAUmspxWoMznSQ.png" /><figcaption>set gh-pages branch as a build source</figcaption></figure><p>And that’s it!</p><p>Of course, you can keep customizing your website endlessly, and I didn’t go over trivial stuff I’m sure you will be able to figure out, such as changing the website title, adding your own photo, and more. The _config.yaml file is very self-explanatory and allows for easy manipulation and customization.</p><p>Thanks, folks! Cheers.</p><p>Bonus tip:</p><ul><li>This GitHub issue helped me set a favicon for the site — <a href="https://github.com/daattali/beautiful-jekyll/issues/312">https://github.com/daattali/beautiful-jekyll/issues/312</a></li></ul><h3>Relevant Links:</h3><ul><li>Jekyll homepage — <a href="https://jekyllrb.com/">https://jekyllrb.com/</a></li><li>Beautiful Jekyll repository — <a href="https://beautifuljekyll.com/">https://beautifuljekyll.com/</a></li><li>jekyll-action GitHub action — <a href="https://github.com/marketplace/actions/jekyll-actions">https://github.com/marketplace/actions/jekyll-actions</a></li><li>Post by <a href="https://medium.com/u/c040a0cea7ab">James Hamman</a> on integration of Jekyll with Medium — <a href="https://medium.com/@jameshamann/displaying-medium-posts-on-your-jekyll-website-7eef230309e4">https://medium.com/@jameshamann/displaying-medium-posts-on-your-jekyll-website-7eef230309e4</a>.</li><li>Explanation on liquid syntax — <a href="https://shopify.github.io/liquid/">https://shopify.github.io/liquid/</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c8f505e3b9e3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/better-programming/how-i-built-a-website-integrated-with-medium-using-jekyll-and-github-pages-c8f505e3b9e3">How I Built a Website Integrating Medium Articles — Using Jekyll and GitHub Pages</a> was originally published in <a href="https://betterprogramming.pub/">Better Programming</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Build a Snake Game Using Go]]></title>
            <link>https://medium.com/better-programming/build-a-snake-game-using-in-go-b4186e21d011?source=rss-d2d1ff8f0490------2</link>
            <guid isPermaLink="false">https://medium.com/p/b4186e21d011</guid>
            <category><![CDATA[game-programming]]></category>
            <category><![CDATA[go]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[golang]]></category>
            <dc:creator><![CDATA[Elad Rapaport]]></dc:creator>
            <pubDate>Wed, 28 Sep 2022 04:34:41 GMT</pubDate>
            <atom:updated>2022-09-28T05:35:03.148Z</atom:updated>
            <content:encoded><![CDATA[<h4>Play your way to learning Go — step-by-step</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*NZqKf6ClGmHh_aOO" /><figcaption>Photo by <a href="https://unsplash.com/@carltraw?utm_source=medium&amp;utm_medium=referral">Carl Raw</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>In this article, we are going to build together the classic game of Snake using Go. This is my first project in Go and what better opportunity to share my learning experience with you and hopefully help your journey to learning Go as well?</p><p>The full code for this project (separated into parts) is in this <a href="https://github.com/erap129/SnakeInGo">GitHub repo</a></p><p>The outline of this post is as follows:</p><ul><li>Part 1— Why Bother?</li><li>Part 2— Setting up our environment</li><li>Part 3— Implementing Snake in Go<br>- Part 3.1— A moving blob<br>- Part 3.2— A moving snake<br>- Part 3.3— Adding food and points</li></ul><h3><strong>Part 1— Why Bother?</strong></h3><p>Why learn a new language? And why not just follow the official tutorial and call it a day?</p><ul><li>Learning a new language makes us better programmers. Every programming language has its set of strengths and features and opening up to languages we haven’t seen before can open us to new ideas and programming paradigms and also add another tool to our toolchain.</li><li>What better way to learn than by games? I personally think it’s way more fun than following an instruction manual.</li></ul><p>So why Go?</p><p>Go is a language created by Google, with version 1.0 being released to the public back in 2012. Since then Go has developed and gained major traction amongst developers. Go is notable for being fast and easy to learn with a strong focus on efficient concurrency (read about <a href="https://golangbot.com/goroutines/">goroutines</a> [2] for more info). I decided that such a popular and trending language deserves a bit of my attention, even if I’m not planning to use it for anything practical in the near future, and thus this tutorial was born!</p><h3>Part 2— Setting up our environment</h3><p>The installation part is relevant only for Linux. Note that it will install an older version of Go (1.13 as of writing this article) but it is sufficient for our project.</p><p><strong>Installation:</strong> Install Go using the following command: sudo apt install golang-go . After following through the installation, check that your system has installed Go by running go version . Your output should look something like this: go version go1.13.8 linux/amd64 (depending on the version you installed). After this is done we can create our first project!</p><p><strong>Creating a project: </strong>We will be using Visual Studio Code (vscode) as our IDE (you can still follow through using other IDEs). Create a new folder for our project called Snake and open up the IDE in this folder. If using vscode you should install the Go extension via the Extensions tab.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/384/1*LeIVvFCosyiH88sD9BxBOg.png" /><figcaption>vscode — Go extension</figcaption></figure><p>Open up the terminal (can be done directly in VS Code), type go mod init Snake , and hit Enter.</p><p>This should create a new file go.mod in your current directory (the Snake directory). If we were publishing this project as a module we would have replaced Snake with a valid URL, such as our GitHub repository, but since this is just a toy project we don’t care about that.</p><p>Let’s just write a simple “Hello World” project to see that everything is up and running properly. Create a new file called main.go in the Snake directory and type in the following code:</p><pre>package main</pre><pre>import &quot;fmt&quot;</pre><pre>func main() {</pre><pre>    fmt.Println(&quot;Hello World!&quot;)</pre><pre>}</pre><p>In this code, we imported and used the standard fmt package which deals with command line input and output. In the terminal run go run main.go. You should see Hello World! printed in your terminal and the program should exit. If this is done we are good to go to the next part — building a Snake game!</p><p>When you want to run a project consisting of several files (as in this game) — from the directory that contains your code run <strong>go run *.go</strong> . What will happen is that your main function will run with awareness of the other files in the project (if you just run go run main.go it will ignore all of the other files and break due to missing dependencies).</p><h3>Part 3— Implementing Snake in Go</h3><p>This implementation is inspired by and based on the <a href="https://earthly.dev/blog/pongo/">Pong Go tutorial</a> [3] created by Josh Aletto.</p><h4><strong>Part 3.1— A moving blob</strong></h4><p>In this first part, we will create a keyboard-controlled blob on the screen. We are starting with this because it is a relatively simple logic task and it will allow us to focus on the basics of the language and the terminal-based GUI framework we will be using — <a href="https://github.com/gdamore/tcell">tcell</a> [4].</p><p>Part I will consist of three files —</p><ul><li>main.go will contain the UI loop which will listen for user input and update the relevant data structures accordingly. It will also initiate the subroutine in game.go.</li><li>game.go deals with the main game loop — updating the state of the different elements in the game and drawing them on the terminal screen.</li><li>snake.go handles the inner state of the snake object (currently just its speed and position)</li></ul><p>Let’s view the files:</p><p>main.go —</p><ul><li>In lines 11–18 we get the screen object from tcell and do some error handling when screen fetching fails.</li><li>In lines 23–33 we define our two objects, the game and the SnakeBody and initialize them with some initial parameters.</li><li>In line 34 we run a goroutine which starts the main loop of the game in a different thread. We will view the game loop later.</li><li>In lines 35–53 we deal with user input. Either the user presses ctrl+c and then we exit the program, or the user presses one of the arrow keys and then we send the relevant signal to the game.SnakeBody object, to change the direction of movement.</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/baed95f3a34eb1e01add84bb30d288f2/href">https://medium.com/media/baed95f3a34eb1e01add84bb30d288f2/href</a></iframe><p>game.go—</p><ul><li>Lines 21–27 define the main game loop. At the start of each step we clear the screen, then we update the snakeBody location and draw it on the screen. After that, we perform time.Sleep to hold the previous frame on the screen (otherwise the movement would be too fast for us to visually perceive). Finally, we draw the new frame onto the screen.</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e904c3b9914ce0152573ae968f6ac554/href">https://medium.com/media/e904c3b9914ce0152573ae968f6ac554/href</a></iframe><p>snake.go —</p><ul><li>This file defines our snake object (currently just a blob) and deals with the changing of direction and the updating of the current location using the current speed. Notice that if the snake disappears into one edge of the screen we want it to reappear at the other edge, hence the complexity in lines 16–23.</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/223d4b5b98db88b9a24bcb8c495aa5de/href">https://medium.com/media/223d4b5b98db88b9a24bcb8c495aa5de/href</a></iframe><p>The gameplay in this part should look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/780/1*tLvO4hv3mkkOBR0PlUOVzg.gif" /><figcaption>Gameplay — part 1</figcaption></figure><h4><strong>Part 3.3— A moving snake</strong></h4><p>To implement a moving snake we will first need to define what being a “snake” means. In this context it means an entity consisting of several parts, where each part has its own X and Y coordinates. By noticing that the movement of the snake can be modelled using a queue we can implement this in a simple and elegant way, as shown in the following figure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1016/1*Gx8FnZX0JZVxyjoZqT2gZA.png" /><figcaption>Snake movement implementation as a queue</figcaption></figure><p>Most of our changes will occur in the file snake.go where the queue logic will be implemented (Go doesn’t have built-in queues so we will implement them using slices). Some minor changes will also happen in main.go and game.gomainly to support the changes in the former.</p><p>main.go —</p><ul><li>In lines 23–36 added an initialization to the snakeParts object which constitutes the initial building blocks of the snake.</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0e65ff9ac5473334d2e5128504e50087/href">https://medium.com/media/0e65ff9ac5473334d2e5128504e50087/href</a></iframe><p>game.go —</p><ul><li>Added the drawParts function which will draw the snake’s parts onto the screen in each iteration of the game loop.</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9b5a5d4734988ec8d21a4ebf909ba045/href">https://medium.com/media/9b5a5d4734988ec8d21a4ebf909ba045/href</a></iframe><p>snake.go—</p><ul><li>Added the Update function which implements the snake’s movement in a queue-like fashion. In each activation of this function a new head is enqueued (depending on the direction of the snake) and the tail’s end is dequeued.</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/163f74371a84aa0039d78f7610b3bd31/href">https://medium.com/media/163f74371a84aa0039d78f7610b3bd31/href</a></iframe><p>The gameplay in this part should look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/780/1*Xz61T_y1iLMkbObe6q7VZg.gif" /><figcaption>Gameplay — part 2</figcaption></figure><h4><strong>Part 3.3— Adding food and points</strong></h4><p>In this part, we will add important logic to the game including:</p><ul><li>The ability to collect food and earn points</li><li>Make the snake longer if it collected food</li><li>Detecting collisions of the snake’s head with its body</li><li>A “Game Over” screen, with the option to replay or to quit</li></ul><p>Let’s discuss the changes required in each of the 3 files:</p><p>snake.go —</p><ul><li>Lines 21–23: check if the longerSnake parameter was passed as true. If so, Don’t perform a dequeue on the snake’s body and thus make it one tile longer.</li><li>Added the ResetPos function for convenience (this function will be called from main.go)</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e40c611cbc648f1a1d60b82ac76d4cc3/href">https://medium.com/media/e40c611cbc648f1a1d60b82ac76d4cc3/href</a></iframe><p>game.go—</p><ul><li>Added drawText function to draw text on the screen. Taken from <a href="https://github.com/liweiyi88/gosnakego">another snake implementation using cell</a> [5].</li><li>Added checkCollision function which receives a List of parts and another part. For each part in the list of parts, it checks whether it collides with the other part. We use this function to check 1) whether the snakehead has collided with food and 2) whether the snakehead has collided with the snake body.</li><li>Added logic in the main loop (I will detail the less trivial additions):<br>1) lines 72–76: check for collision with food and update score if collision detected. Also, redraw food in a random location.<br>2) lines 86–88: Game Over screen. Notice that after displaying the score this subroutine will terminate (In main.go the user will have an option to start another goroutine with a fresh game)</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/240472b32dc753b5e03d44dcac97dd93/href">https://medium.com/media/240472b32dc753b5e03d44dcac97dd93/href</a></iframe><p>main.go —</p><ul><li>Lines 43–47: These lines deal with the Game Over situation. If a game over is detected (using the GameOver bool in game), the user has an option to press y or n and the game will either restart or terminate accordingly. We restart the game simply by initiating another goroutine.</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6366550700086ad27035c763e14be0b5/href">https://medium.com/media/6366550700086ad27035c763e14be0b5/href</a></iframe><p>The gameplay in this part should look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/780/1*p3yyUi8OQ3KX2i1F7Dtljg.gif" /><figcaption>Gameplay — part 3</figcaption></figure><p>And we’re done with the implementation! Your snake game is up and running and you can invite friends over for a tournament :)</p><h3>Thoughts Ahead</h3><p>I hope you had fun implementing Snake in Go, I know I did. I think goroutines are a super-cool feature, I don’t know other languages where it is that easy to perform parallelization. Although this game is fairly simple I still liked the queue implementation of movement as a small mental exercise. This experience will also shorten the learning curve if I ever have to use this language for anything practical, and it’s always good to have another tool under my belt.</p><p>If I had more time I would extend this game and add cool power-ups to my snakes such as particle-shooting abilities, speed changing, and other goodies. I would also add unit tests to make sure that nothing breaks after these feature additions.</p><p>Thanks for reading, see you next time!</p><h3>References</h3><p>[1] — Full project code: <a href="https://github.com/erap129/SnakeInGo">https://github.com/erap129/SnakeInGo</a></p><p>[2] —goroutines: <a href="https://golangbot.com/goroutines/">https://golangbot.com/goroutines/</a></p><p>[3] — Pong go tutorial: <a href="https://earthly.dev/blog/pongo/">https://earthly.dev/blog/pongo/</a></p><p>[4] — tcell Go package: <a href="https://github.com/gdamore/tcell">https://github.com/gdamore/tcell</a></p><p>[5] — Another snake implementation using tcell: <a href="https://github.com/liweiyi88/gosnakego">https://github.com/liweiyi88/gosnakego</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b4186e21d011" width="1" height="1" alt=""><hr><p><a href="https://medium.com/better-programming/build-a-snake-game-using-in-go-b4186e21d011">Build a Snake Game Using Go</a> was originally published in <a href="https://betterprogramming.pub/">Better Programming</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MovieLens-1M Deep Dive — Part II,  Tensorflow Recommenders]]></title>
            <link>https://medium.com/data-science/movielens-1m-deep-dive-part-ii-tensorflow-recommenders-4ca358cc886e?source=rss-d2d1ff8f0490------2</link>
            <guid isPermaLink="false">https://medium.com/p/4ca358cc886e</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[tensorflow]]></category>
            <category><![CDATA[recommender-systems]]></category>
            <dc:creator><![CDATA[Elad Rapaport]]></dc:creator>
            <pubDate>Mon, 12 Sep 2022 15:03:52 GMT</pubDate>
            <atom:updated>2022-09-12T21:08:19.965Z</atom:updated>
            <content:encoded><![CDATA[<h3>MovieLens-1M Deep Dive — Part II, Tensorflow Recommenders</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JbAFlnuM2VOBZt0tKjf5FA.jpeg" /><figcaption>Photo by Nathan Engel: <a href="https://www.pexels.com/photo/time-lapse-photography-of-car-lights-in-front-of-cinema-436413/">https://www.pexels.com/photo/time-lapse-photography-of-car-lights-in-front-of-cinema-436413/</a></figcaption></figure><p>Hello readers,</p><p>For those of you who haven’t read the previous part, here is the link:</p><p><a href="https://towardsdatascience.com/movielens-1m-deep-dive-part-i-8acfeda1ad4">MovieLens-1M Deep Dive — Part I</a></p><p>In that article, I present the MovieLens-1M [1] dataset (a movie recommendations dataset that contains 1 million ratings for movies made by different users) along with some exploratory data analysis and try out some classical recommender systems algorithms. Although, that article is not a pre-requisite and you will be able to understand the content here without reading it.</p><p>In the second installment of the MovieLens deep dive series, we will use TensorFlow recommenders (TFRS). TFRS will allow us to abstract away much of the gory details involved when dealing with recommender systems, such as evaluation metrics and the model training procedure. Our model will be based on the two-tower architecture, which we will review before delving into the code.</p><p>In the coding sections, I will somewhat extend an existing official TFRS tutorial — <a href="https://www.tensorflow.org/recommenders/examples/deep_recommenders">Building Deep Retrieval Models</a> [3]. In this tutorial, the goal is to build a retrieval recommender system using rich context features of the items (movies) and the users. We will test the impact of adding different features and try to gain improvements by introducing smarter and more relevant features to the task at hand.</p><p>The outline of the article is as follows:</p><ol><li>A brief overview of modern recommender system methods</li><li>Get to know TFRS and the retrieval task — the basic building blocks which we will be using</li><li>Beat the benchmark — attempt to outperform the original TFRS tutorial model by using more contextual features</li><li>Beat the benchmark — results</li><li>Conclusion and thoughts ahead</li></ol><p>Let’s dive in!</p><h3>Part I — A Brief Overview of Modern Recommender System Methods</h3><p>The recommender systems pipeline is usually split into two stages — retrieval and ranking. In the retrieval stage, we generate representations for items and users in our system (usually in the form of embeddings) and select a subset of items that the user might be interested in. This stage may deal with millions of users/items and thus must be computationally efficient. In the ranking stage, we take the output of the retrieval stage and rank them to select the final few items that will be suggested to the user. This model may be more computationally intensive as it must deal with far fewer data. A depiction of this process is shown in figure 1.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eDYJDXmV7VKSN5yBUyuHVw.png" /><figcaption>Fig. 1: The recommender systems process</figcaption></figure><p>In this walkthrough, we will focus on the retrieval stage, and try to make it more efficient and accurate by taking into account different contextual features in the embedding generation process.</p><p>Now for some terminology:</p><ul><li>Items — These are the items that we will recommend to users. In our case — movies.</li><li>Queries — This is the information our system will use to make item recommendations. In our case, this will include the ID of the user we are recommending and other features such as the age and gender of the user. We will also include it here</li></ul><h3>Part II — Get to know TFRS and the retrieval task</h3><p>Enter TFRS, a recommender systems library built on top of Tensorflow, intended on streamlining the process of building, training, and deploying recommender systems.</p><p>The models we build in TFRS will inherit from the <a href="https://www.tensorflow.org/recommenders/api_docs/python/tfrs/models/Model">tfrs. model.Model</a> class. This class is itself a wrapper around tf.keras.Modeland saves us the hassle of writing the train_step and test_step methods. What we as users have to do is write the compute_loss function, which receives a batch of input queries and items and computes the loss for the model.</p><p>In our tutorial, we will build a two-tower architecture model. This model contains one tower for the items and one tower for the queries. As you can see in figure 2, the two-tower architecture is pretty simple. Embeddings are created to represent the queries and the users, and eventually, we perform a cosine similarity operation on the two embedding vectors to predict their affinity (is the query a good match for this item?). By sorting the results of a certain query using the cosine similarity with each item embedding the system can retrieve the top items for this user (usually in the order of 1000&#39;s), which will be then most likely fed into a ranking model for fine-grained control over the final items that will be presented to the user (usually up to 10 items).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XQ565jpYkqeGbw_vjfl3hw.png" /><figcaption>Fig. 2: Simple illustration of the two-tower architecture</figcaption></figure><p>What we don’t understand from the explanation above though is, how does the model learn? Well, of course by gradient descent. There are positive examples (in our case, movies that were rated by a certain user) and negative examples (movies that were not rated by a certain user). Our algorithm will treat this as a multiclass classification task and try to maximize the similarity between the rated items and the query and minimize the similarity between non-rated items and the query. This operation is done per batch (each batch consists of only positive examples, and the negative examples are generated by pairing the different queries and items in the batch, as shown in figure 3).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nSEuImpGi8JfgPn4juNyLw.png" /><figcaption>Fig. 3: The retrieval task (FFN=feedforward network)</figcaption></figure><h3>Part III — Beat the benchmark</h3><p>In this part, I will present my code to you and we will walk through the process of trying to improve the performance of our deep retrieval model, by enriching the item and query representations using additional context features.</p><p>Let’s review the features we will use for both items and queries. For each feature, I will specify if it was used in the original TFRS tutorial or not.</p><p><strong>Query features (user + context info):</strong></p><ul><li>user_id [int] — the most basic feature. Used to index into the user embedding matrix. (Used in original tutorial)</li><li>timestamp [int] — The time of the query performed by the user, represented in UNIX timestamp format. This can help us capture seasonal trends in the data (for example — on weekends people might prefer longer movies). (Used in original tutorial)</li><li>user_age [int] — An additional numeric feature that might hint into patterns in user preferences. (Not used in original tutorial)</li></ul><p><strong>Item features (movie info):</strong></p><ul><li>movie_title [string] — A unique identifier for items. Used to index into the item embedding matrix. This feature will also be used to create a sentence embedding of the movie title text (as opposed to just using it as a plain identifier). (Used in original tutorial)</li><li>movie_genres [list&lt;int&gt;] — Each movie is associated with one or more genres, where each genre is defined by a unique integer. We will use these integers to index into a genre embedding matrix. For each movie, we will average the embeddings for all of its genres. (Not used in original tutorial)</li><li>movie_length [int] — The length of the movie in minutes. This info was collected using the <a href="https://cinemagoer.github.io/">Cinemagoer</a>[2] python package. Some of the movie length data was missing and for these movies, I gave a default length of 90 minutes.</li></ul><p><strong>The code:</strong></p><p>I present here a non-conventional way of doing exploratory data science work, and that is via scripting and not via Jupyter notebooks. I think the advantages of scripting VS notebooks are:</p><p>1) Cleaner code. Encourages you to use good programming practices, such as extracting common procedures into functions instead of copy-pasting between cells.</p><p>2) The ability to track your work on it in a sane way, which is of utmost importance, especially when collaborating with other people.</p><p>The downside is that it might introduce the need for caching mechanisms, as I have done in this case (so you won’t have to run a heavy data loading/model training process for each experiment you wish to perform). In this example, I save the trained models for each configuration so I won’t have to re-run training every time I run the script. Additionally, I save and load the dataset using tf.data.Dataset.save and tf.data.Dataset.load because the dataset creation process can take a while. Finally, I utilize the <a href="https://pypi.org/project/cachier/">cachier</a> python package, which generates persistent caching of function results on disk, when loading the movie length information from Cinemagoer as this is a very lengthy process and we want to avoid running this function more than once for each movie.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/4b2f8c771087dc9526d16fc3ab9f7d9a/href">https://medium.com/media/4b2f8c771087dc9526d16fc3ab9f7d9a/href</a></iframe><p>The required non-standard packages to run this script are as follows: tensorflow, tensorflow_datasets, tensorflow_recommenders, plotly, numpy . Note that the script assumes you have two sub-directories in your main directory named datasets and saved_models.</p><p>The training process will be managed by the MovieLensTrainer class. Given training settings such as layer sizes, embedding size, and which features to use, it will either train models or load the relevant models from the disk. After obtaining the trained models for all of the feature combinations we defined via the command-line arguments, we will run the function plot_training_runs which will return a plotly line chart of the top-100 categorical accuracy of each run and will allow us to compare the different settings we used.</p><p>An important change I made to the model from the original TFRS tutorial is the ability to use additional features in the MovieModel (the item tower). In the tutorial, this model takes just the movie_title as input. In this project it can take any additional feature, eventually creating a concatenation of the embeddings of all features. This is important as it enables us to leverage additional movie information such as the genres, movie length, average rating on IMDB, and any other feature you may think of.</p><h4>Command line arguments + Run example</h4><p>We have several command-line arguments that control what our script does. I will go over the less obvious ones:</p><p>layer_sizes (list[int]) — This defines the number and size of the layers following the embedding layer for each of the two towers (query and item models). The more layers we add and the more neurons we give them, the more complex our model will become (and thus more prone to overfitting as well)</p><p>additional_feature_sets (list[list[string]]) — This argument is a list of lists, where each inner list is a set of features we wish to train a TFRS model on. For example, using this argument we can train two models, where one trains using the movie_genres, timestamp features, and the other trains using movie_title_text, timestamp features.</p><p>--retrain (bool) — This is a binary toggle. When off, we will load a previously trained model with the selected settings if we already trained before (loaded from the file system). If on, we will always re-train and overwrite the models currently on the disk.</p><p>--generate_recommendations_for_user (int) — This is an ID of a user for which we will generate recommendations for each trained model that we produced. This is meant to bring our model “down to earth” and see real examples of the recommendations it generates for a user whose preferences are known.</p><p>Example of a set of command line arguments for this process (this run will train 6 different models, each for 300 epochs. It will generate recommendations for a user with an ID of 1 for each trained model):</p><pre>--num_epochs<br>300<br>--additional_feature_sets<br>None<br>--additional_feature_sets<br>timestamp<br>--additional_feature_sets<br>movie_title_text<br>--additional_feature_sets<br>timestamp<br>movie_title_text<br>--additional_feature_sets<br>timestamp<br>movie_title_text<br>movie_genres<br>--additional_feature_sets<br>timestamp<br>movie_title_text<br>movie_genres<br>movie_length<br>--embedding_size<br>32<br>--layer_sizes<br>64<br>32<br>--generate_recommendations_for_user<br>1</pre><p>Let’s view the output of this run (removed warnings for clarity):</p><p>These are the ratings for user 1:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a6fc7d2e8d577ab1373f7c846997f8da/href">https://medium.com/media/a6fc7d2e8d577ab1373f7c846997f8da/href</a></iframe><pre><br><strong>Recommendations for model (&#39;timestamp&#39;,), user 1:</strong><br> [b&#39;Alien (1979)&#39; b&#39;Sense and Sensibility (1995)&#39; b&#39;Sea Wolves, The (1980)&#39;<br> b&#39;Ninth Gate, The (2000)&#39; b&#39;Beautiful (2000)&#39; b&#39;Sweet Nothing (1995)&#39;<br> b&#39;Titan A.E. (2000)&#39; b&#39;Saint, The (1997)&#39;<br> b&#39;Killer: A Journal of Murder (1995)&#39; b&#39;Man of the Year (1995)&#39;]<br>Recommendations for model (&#39;movie_title_text&#39;,), user 1:<br> [b&#39;Angel and the Badman (1947)&#39; b&#39;Alien (1979)&#39; b&#39;Shadows (Cienie) (1988)&#39;<br> b&#39;Two Moon Juction (1988)&#39; b&#39;Sea Wolves, The (1980)&#39; b&#39;Booty Call (1997)&#39;<br> b&#39;Apple Dumpling Gang Rides Again, The (1979)&#39;<br> b&#39;Psycho Beach Party (2000)&#39; b&#39;Local Hero (1983)&#39;<br> b&quot;Shaft&#39;s Big Score! (1972)&quot;]<br><strong>Recommendations for model (&#39;timestamp&#39;, &#39;movie_title_text&#39;), user 1:</strong><br> [b&#39;Fried Green Tomatoes (1991)&#39; b&#39;Alien (1979)&#39;<br> b&#39;Angel and the Badman (1947)&#39; b&#39;Sarafina! (1992)&#39;<br> b&#39;Color Purple, The (1985)&#39; b&#39;Sea Wolves, The (1980)&#39;<br> b&#39;Madame Sousatzka (1988)&#39; b&#39;Little Shop of Horrors (1986)&#39;<br> b&quot;One Flew Over the Cuckoo&#39;s Nest (1975)&quot; b&#39;L.A. Story (1991)&#39;]<br><strong>Recommendations for model (&#39;timestamp&#39;, &#39;movie_title_text&#39;, &#39;movie_genres&#39;), user 1:<br></strong> [b&#39;Being There (1979)&#39; b&#39;Alien (1979)&#39; b&#39;Crimes and Misdemeanors (1989)&#39;<br> b&#39;Two Moon Juction (1988)&#39; b&#39;Psycho (1998)&#39;<br> b&#39;Apple Dumpling Gang Rides Again, The (1979)&#39; b&#39;Me Myself I (2000)&#39;<br> b&#39;Loser (2000)&#39; b&#39;Killer: A Journal of Murder (1995)&#39;<br> b&quot;Barney&#39;s Great Adventure (1998)&quot;]<br><strong>Recommendations for model (&#39;timestamp&#39;, &#39;movie_title_text&#39;, &#39;movie_genres&#39;, &#39;movie_length&#39;), user 1:<br></strong> [b&#39;Sid and Nancy (1986)&#39; b&#39;Angel and the Badman (1947)&#39; b&#39;Alien (1979)&#39;<br> b&#39;Two Moon Juction (1988)&#39; b&#39;Crimes of the Heart (1986)&#39;<br> b&#39;Sea Wolves, The (1980)&#39; b&#39;One False Move (1991)&#39;<br> b&#39;Rich and Strange (1932)&#39; b&#39;Storefront Hitchcock (1997)&#39;<br> b&#39;Celebration, The (Festen) (1998)&#39;]<br><strong>Recommendations for model (), user 1:</strong><br> [b&#39;Alien (1979)&#39; b&#39;Booty Call (1997)&#39;<br> b&#39;Day the Earth Stood Still, The (1951)&#39; b&#39;Angel and the Badman (1947)&#39;<br> b&#39;Sea Wolves, The (1980)&#39; b&#39;Celebration, The (Festen) (1998)&#39;<br> b&#39;Midnight Cowboy (1969)&#39; b&#39;Desperado (1995)&#39; b&#39;Tingler, The (1959)&#39;<br> b&#39;Blue Chips (1994)&#39;]</pre><p>This is just a personal thought, but as user 1 I think I would be pretty disappointed. The only children’s movie that appears in all of the recommendations is “Barney’s Great Adventure (1998)”, even though the user has a clear affinity for these kinds of movies. Plus, we can even see some inappropriate movies (user 1 is categorized as age K-12). Some more modeling improvement and placement of heuristics are needed before this system is production-ready.</p><p>Now, let’s view some accuracy results and see if we managed to beat the tutorial benchmark by including more features in the training process.</p><h3>Part IV — Beat the benchmark — Results</h3><p>We will train models with the following feature combinations:</p><ol><li>movie_title, user_id</li><li>movie_title, user_id, timestamp</li><li>movie_title, user_id, movie_title_text</li><li>movie_title, user_id, timestamp, movie_title_text <strong>(this setting was used in the official tutorial)</strong></li><li>movie_title, user_id, timestamp, movie_title_text, movie_genres</li><li>movie_title, user_id, timestamp, movie_title_text, movie_genres, movie_length</li></ol><p>And use the following layer sizes:</p><p>[64, 32]</p><p>For a total of 6 trained models. We will train each model for 300 epochs and use an embedding size of 32 for all embedding layers. I hypothesize that the additional features will enrich the item/query embeddings and help generate more accurate representations. The metric we will use to compare the models is top-100 accuracy (higher = better). This means, for each item/query pair in the validation set — was the affinity of the pair in the top-100 pairs generated for this query?</p><p>Shown in figure 4 (included a static image as well as an interactive graph because of visibility issues on mobile) is the validation top-100 accuracy (in 5 epoch intervals) for each model training.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*szA0EDkEUgEjgV07NE0glQ.png" /><figcaption>Fig. 4 (static): Model comparison results</figcaption></figure><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatapane.com%2Freports%2F9ArQBZk%2Fmovielens-1m-tfrs%2Fembed%2F&amp;display_name=Datapane&amp;url=https%3A%2F%2Fdatapane.com%2Freports%2F9ArQBZk%2Fmovielens-1m-tfrs%2F&amp;image=https%3A%2F%2Fdatapane.com%2Fstatic%2Fpython_report_illustration.png&amp;key=d04bfffea46d4aeda930ec88cc64b87c&amp;type=text%2Fhtml&amp;schema=datapane" width="960" height="700" frameborder="0" scrolling="no"><a href="https://medium.com/media/7d851165a03ff9f9ba06bc1dea27de1d/href">https://medium.com/media/7d851165a03ff9f9ba06bc1dea27de1d/href</a></iframe><p>We can see consistently that as we add more features, the validation accuracy increases. This is encouraging as it means our work had a positive effect! Note that the overall accuracy seems pretty low, this is due to this being a very difficult problem (need to guess the correct movie in the top-100 results out of around 6000 movies). The best approach in terms of validation accuracy was the one that used all of the proposed features, and indeed it beat the benchmark from the tutorial by about 0.015 after 300 epochs.</p><h3>Part V — Conclusions and thoughts ahead</h3><p>In this article we have seen:</p><ol><li>How to build a recommendation system using tensorflow recommenders.</li><li>How to add context features to both the items and the queries to achieve better performance.</li><li>How to perform research via plain scripting rather than using Jupyter notebooks.</li></ol><p>We can see that additional context, both for the queries and for the items, can boost our recommendation performance (even on relatively dense datasets such as MovieLens-1M). When building your own recommender systems try to think if you can add some more context to your embeddings rather than just using user and item identifiers :)</p><p>See you next time,</p><p>Elad</p><p><strong>References:</strong></p><p>[1]<em> Maxwell Harper and Joseph A. Konstan. 2015. The MovieLens Datasets: History and Context. ACM Transactions on Interactive Intelligent Systems (TiiS) 5, 4: 19:1–19:19. </em><a href="https://doi.org/10.1145/2827872"><em>https://doi.org/10.1145/2827872</em></a></p><p>[2] Tensorflow deep retrieval systems tutorial — <a href="https://www.tensorflow.org/recommenders/examples/deep_recommenders">https://www.tensorflow.org/recommenders/examples/deep_recommenders</a></p><p>[3] Cinemagoer python package — <a href="https://cinemagoer.github.io/">https://cinemagoer.github.io/</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4ca358cc886e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/movielens-1m-deep-dive-part-ii-tensorflow-recommenders-4ca358cc886e">MovieLens-1M Deep Dive — Part II,  Tensorflow Recommenders</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[MovieLens-1M Deep Dive — Part I]]></title>
            <link>https://medium.com/data-science/movielens-1m-deep-dive-part-i-8acfeda1ad4?source=rss-d2d1ff8f0490------2</link>
            <guid isPermaLink="false">https://medium.com/p/8acfeda1ad4</guid>
            <category><![CDATA[surprise]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[recommendation-system]]></category>
            <category><![CDATA[recommender-systems]]></category>
            <dc:creator><![CDATA[Elad Rapaport]]></dc:creator>
            <pubDate>Wed, 08 Jun 2022 17:30:47 GMT</pubDate>
            <atom:updated>2022-06-08T17:30:47.739Z</atom:updated>
            <content:encoded><![CDATA[<h3>MovieLens-1M Deep Dive — Part I</h3><h4>A hands-on recommendation systems tour using the popular benchmark dataset</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iDGcwsySEgnvyrm8PtQT3g.jpeg" /><figcaption>Photo from <a href="https://www.pexels.com/photo/food-snack-popcorn-movie-theater-33129/">pexels</a></figcaption></figure><p>Recommendations. We all consume them. Be it through our favorite movie streaming apps, online shopping, or even passively as a target of advertising campaigns. How are these recommendations created? How does a recommendation system utilize enormous datasets of internet transactions to generate high quality and personalized recommendations? I find these questions fascinating, hence I decided to embark on a learning journey and this post is intended to share my findings with you, dear readers :)</p><p>I find that there are sufficient theoretical blog posts on the basics of recommendation systems and decided that this will be a hands-on practical tour of one of the most popular recommendation-focused datasets available — <a href="https://grouplens.org/datasets/movielens/1m/">MovieLens-1M</a> [1] (used with permission). In this dataset, we are given ~1 million historical ratings of 2894 movies by 6040 unique users.</p><p>The structure of this post is as follows:</p><ul><li>Part I — Exploratory data analysis (EDA)</li><li>Part II — Data pre-processing (retrieving movie content information)</li><li>Part III — Setting up the recommendation problem and obtaining a baseline score</li><li>Part IV— Recommending movies with collaborative filtering</li><li>Part V — Recommending movies with content-based filtering</li><li>Part VI — Conclusions and thoughts ahead</li></ul><p>The notebook containing all of this project’s code and more is available here: <a href="https://colab.research.google.com/drive/132N2lGQTT1NnzAU6cg1IfwpaeFKK9QQe?usp=sharing">https://colab.research.google.com/drive/132N2lGQTT1NnzAU6cg1IfwpaeFKK9QQe?usp=sharing</a></p><p>This is the first of a two-part deep dive into MovieLens-1M. The next part, which will be released in the near future, will focus on more advanced algorithms and continue the never-ending EDA process. Happy reading!</p><h3>Part I — EDA</h3><p>First, download the MovieLens-1M dataset from <a href="https://grouplens.org/datasets/movielens/1m/">here</a>. Then, let’s make some necessary imports and get that out of the way for this project:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/5457a2a7f9545da680f2f38e51cb53a7/href">https://medium.com/media/5457a2a7f9545da680f2f38e51cb53a7/href</a></iframe><p>The MovieLens-1M dataset consists of 3 files — users.dat, ratings.dat and movies.dat. Let’s explore each of these files and understand what we are dealing with. We will start with the movie&#39;s data.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/034ecd15ab58464f82b1340676d78404/href">https://medium.com/media/034ecd15ab58464f82b1340676d78404/href</a></iframe><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/d706c36c2998588808611929068fb8b9/href">https://medium.com/media/d706c36c2998588808611929068fb8b9/href</a></iframe><p>Let’s plot the distribution of movies per genre. For this, we will have to turn the genres column into a list and “explode” it. Note that each movie may be of several genres, and in this case, it will be counted multiple times. We will also plot the distribution of movies per movie release year.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/92b251d5ea431508279a9690ca26d8d5/href">https://medium.com/media/92b251d5ea431508279a9690ca26d8d5/href</a></iframe><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatapane.com%2Fu%2Felad%2Freports%2Fv7Jb297%2Fmovie-count-by-genre%2Fembed%2F&amp;display_name=Datapane&amp;url=https%3A%2F%2Fdatapane.com%2Fu%2Felad%2Freports%2Fv7Jb297%2Fmovie-count-by-genre%2F&amp;image=https%3A%2F%2Fdatapane.com%2Fstatic%2Fpython_report_illustration.png&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=datapane" width="960" height="700" frameborder="0" scrolling="no"><a href="https://medium.com/media/edc01c5445a19c2eaf79fd9c94ad3364/href">https://medium.com/media/edc01c5445a19c2eaf79fd9c94ad3364/href</a></iframe><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f69c7659ed86bab810dc5f0ccdfc8b8e/href">https://medium.com/media/f69c7659ed86bab810dc5f0ccdfc8b8e/href</a></iframe><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatapane.com%2Fu%2Felad%2Freports%2FVkBgW8A%2Fmovie-count-by-year%2Fembed%2F&amp;display_name=Datapane&amp;url=https%3A%2F%2Fdatapane.com%2Fu%2Felad%2Freports%2FVkBgW8A%2Fmovie-count-by-year%2F&amp;image=https%3A%2F%2Fdatapane.com%2Fstatic%2Fpython_report_illustration.png&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=datapane" width="960" height="700" frameborder="0" scrolling="no"><a href="https://medium.com/media/bca43dcd567845564eff025272b7b527/href">https://medium.com/media/bca43dcd567845564eff025272b7b527/href</a></iframe><p>Let’s move on to the users’ data and inspect it. The “occupation” column is encoded with a number representing each occupation, but for EDA we are interested in the actual data. We will get it by extracting the relevant rows from the README file of the dataset and swapping values in the dataset accordingly.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/da88426646db26797097a3f7054e4341/href">https://medium.com/media/da88426646db26797097a3f7054e4341/href</a></iframe><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/5412807928a4ba0e95315d913ea063e5/href">https://medium.com/media/5412807928a4ba0e95315d913ea063e5/href</a></iframe><p>Let’s view the ratings now.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/55e0770900b0f70de46ac6ea91c715f4/href">https://medium.com/media/55e0770900b0f70de46ac6ea91c715f4/href</a></iframe><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e4b729dcd2ff8ace4773adcbf01b1c51/href">https://medium.com/media/e4b729dcd2ff8ace4773adcbf01b1c51/href</a></iframe><p>Let’s rank movie genres by their average rating. We will also plot how many times each genre was rated.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1d557661e0921e862ce8abfdbff4f409/href">https://medium.com/media/1d557661e0921e862ce8abfdbff4f409/href</a></iframe><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatapane.com%2Fu%2Felad%2Freports%2FaAMbQW3%2Fratings-by-genre%2Fembed%2F&amp;display_name=Datapane&amp;url=https%3A%2F%2Fdatapane.com%2Fu%2Felad%2Freports%2FaAMbQW3%2Fratings-by-genre%2F&amp;image=https%3A%2F%2Fstorage.googleapis.com%2Fdatapane-files-prod%2Fdp%2Fthumbnails%2F87716df9-6c9a-4b6a-9f71-f412fa6cb226.png&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=datapane" width="960" height="700" frameborder="0" scrolling="no"><a href="https://medium.com/media/0e6fa4828d29ed920755b98b700b8a73/href">https://medium.com/media/0e6fa4828d29ed920755b98b700b8a73/href</a></iframe><p>Now let’s combine all three tables (movies, users, and ratings) and see if there are any differences between male and female ratings per movie genre. Notice that here too we will normalize by the total amount of males/females to get a better answer to our question.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/8ccf0f5ad4ed01c8528a23e6f5ec60e3/href">https://medium.com/media/8ccf0f5ad4ed01c8528a23e6f5ec60e3/href</a></iframe><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatapane.com%2Fu%2Felad%2Freports%2Fj3LYl47%2Fratings-by-gender-and-genre%2Fembed%2F&amp;display_name=Datapane&amp;url=https%3A%2F%2Fdatapane.com%2Fu%2Felad%2Freports%2Fj3LYl47%2Fratings-by-gender-and-genre%2F&amp;image=https%3A%2F%2Fdatapane.com%2Fstatic%2Fpython_report_illustration.png&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=datapane" width="960" height="700" frameborder="0" scrolling="no"><a href="https://medium.com/media/4e62b19af3561df82bde93e564f4bf67/href">https://medium.com/media/4e62b19af3561df82bde93e564f4bf67/href</a></iframe><p>Again, we see some typical male/female differences in this dataset (men gave higher ratings to actions movies than women and vice versa for romance movies). This is a good sanity check for the data as the plots make sense.</p><h3>Part II — Data pre-processing (retrieving movie content information)</h3><p>In this part we will retrieve an important data source that will help us recommend movies later, and this is the movie plot summaries. We will obtain them from Wikipedia, using the <a href="https://pypi.org/project/wikipedia/">Wikipedia python package</a>.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a92dc02cd7d40472cf0ed844f7b20b13/href">https://medium.com/media/a92dc02cd7d40472cf0ed844f7b20b13/href</a></iframe><p>There are 615 NaN movie plots</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f7c1376e79484c49a3559ff35e2d7eeb/href">https://medium.com/media/f7c1376e79484c49a3559ff35e2d7eeb/href</a></iframe><p>We see that we were not able to obtain movie plots for 615 movies. This might be because their Wikipedia page doesn’t have a “Plot” category or for some other reason. Since web crawling isn’t the main point here, we’ll just drop these movies and all ratings associated with them.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ccf0172f91a763d0f6cd34e1f53b47b5/href">https://medium.com/media/ccf0172f91a763d0f6cd34e1f53b47b5/href</a></iframe><h3>Part III — Setting up the recommendation problem and obtaining a baseline score</h3><p>We will choose a user that will be acase study for which we will make actual predictions and see if the make sense to us. Most conveniently, we will choose the user with ID #1. Shown below in the user and the movies that she rated. We can see that she is a female K-12 student (age of 1 is an error in the data but I don’t think it’s very important anyhow). The top-20 movies she likes are mainly classical children’s hits with some anomalies here and there (Schindler’s List, One Flew Over the Cuckoo’s Nest).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/555/1*EtuZbHe2-d2yY56u0YA9mQ.png" /></figure><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6084a01144a6898623ec6b4ac68f2b4d/href">https://medium.com/media/6084a01144a6898623ec6b4ac68f2b4d/href</a></iframe><p>We will also shine our spotlight on the movie Toy Story (1995), and for each prediction method we will check which are the movies that are closest to this movie in the embedding space.</p><p>Finally, let’s start recommending stuff! We will create two dataset splits:</p><p>1. Standard train/test split. This will be used for the rating prediction regression task</p><p>2. Leave-one-out cross-validation split. This will be used for hit-rate prediction (which is generally regarded as a more relevant metric in the field of recommender systems). The split is performed by taking one movie per user out of the dataset and using this as a train set. All the other user’s movies will be on the trainset. Then, the hit rate is the percentage of times the movie we took out was in the top-K ratings for that user.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/42a4ce204ab4396b247f048ceda63abc/href">https://medium.com/media/42a4ce204ab4396b247f048ceda63abc/href</a></iframe><p>For each method we will calculate:</p><ul><li>RMSE on test set</li><li>hit-rate on leave-one-out cross validation set</li><li>Top predictions for user #1 (case study)</li><li>Top-10 most similar movies to Toy Story (1995)</li></ul><p>Now, let’s define some auxiliary functions to help us evaluate our algorithms (mainly — the hit rate). We will inspect two measures for each algorithm — the RMSE and hit rate. Some of the code here is taken from the fabulous course on recommender systems by <a href="https://sundog-education.com/recsys/">Sundog Education</a>. I will also implement a simple tweak to the original KNN and SVD algorithms because calculating the hit rate takes a long time and I want to at least be able to measure the progress in real-time.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a14edc9471d0f4e4578a73367f831113/href">https://medium.com/media/a14edc9471d0f4e4578a73367f831113/href</a></iframe><p>Let’s obtain results for a completely random recommender system, so we will be able to evaluate if our algorithms are better or worse than random (after all, we want to make things better not worse…)</p><ul><li>Note — hit rate takes a lot of time to compute</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a7c0974a38b4f0be9003907647337756/href">https://medium.com/media/a7c0974a38b4f0be9003907647337756/href</a></iframe><pre>RMSE: 1.4960<br>HitRate: 0.02185430463576159</pre><h3>Part IV — Recommending movies with collaborative filtering</h3><p>We will use the classic vanilla algorithms, out-of-the-box from the Surprise package (with some minor tweaks to show progress in KNN). I won’t go into the algorithmic details here because I think there are many great tutorials out there for learning these things (just google recommender systems SVD/KNN and you&#39;re good to go). I will check these algorithms:</p><ol><li>SVD</li><li>User-based KNN</li><li>Item-based KNN</li></ol><p>We will start with SVD:</p><pre>RMSE: 0.8809<br>HitRate: 0.03923841059602649</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/762/1*QA8122cc7PwbcuDZe8GYpw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/658/1*sK5FCZe54WvUuLryVdEP5g.png" /></figure><p>Nice! That’s a major improvement over the random score. But when we look at the predictions for user #1 they still feel a bit “off”. The only children’s movie is Babe (1995) and I personally would expect more of these to appear in a good recommendation for this user.</p><p>Now onto KNN. We will start with user-based KNN. This means that predictions for movie <em>m</em> and user <em>u</em> will be based upon the tastes of users that are similar to <em>u</em>.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/8e42c76833f5faa9d4b4994d0e7250d2/href">https://medium.com/media/8e42c76833f5faa9d4b4994d0e7250d2/href</a></iframe><pre>RMSE: 0.9595<br>HitRate: 0.002152317880794702</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/656/1*WLGhvvYSlI0JJzp_EmDopg.png" /></figure><p>The RMSE is better than random but the hit rate is worse. We can also see that the top predictions for user_1 seem even further away than those that were given by the SVD algorithm. In user-based KNN there is no notion of similarity between items, so we won’t calculate the most similar movies to Toy Story (1995) for this algorithm.</p><p>Let us now check the performance of item-based KNN, which recommends items to users by the similarity of the items they already rated to other items they have not yet rated. Code-wise only a slight change to the class instantiation is required.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/632f7953b5ab1b624276cd03beec790b/href">https://medium.com/media/632f7953b5ab1b624276cd03beec790b/href</a></iframe><figure><img alt="" src="https://cdn-images-1.medium.com/max/869/1*tU1iUEHRFmQPwoF6t_rv6w.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/713/1*DyzxsHX5EVQQ4GobjbOnDA.png" /></figure><pre>RMSE: 0.9851<br>HitRate: 0.002152317880794702</pre><p>The RMSE is a bit worse than the user-based KNN and the hit rate reaches almost zero. The most similar movies to Toy Story (1995) seem reasonable but the predictions for user_1 are once again unsatisfying. So far the KNN algorithms have failed to outperform the SVD, which really is regarded as a much better algorithm for collaborative filtering.</p><p>Up until now, we haven’t taken into account the features of the movies themselves. Maybe if I know something about the details of the movies it will help me make better recommendations? Let’s check it out. This is called content-based filtering.</p><h3>Part V — Recommending movies with content-based filtering</h3><p>For the content-based filtering we will use KNN-based algorithms in three approaches (two of them item-based and one user-based):</p><p>1. <strong>Movie plots (item-based)</strong>: Create a vector representation of all of the movies based on the plot descriptions. We will do this by first stemming all of the words in the plot description and then applying TF-IDF to vectorize each document. The similarity matrices we will generate will be based on:</p><p>a. Using the complete TF-IDF matrix</p><p>b. Using the TF-IDF matrix after feature selection</p><p>c. Using the TF-IDF matrix after feature selection and after removing peoples’ names</p><p>2. <strong>Movie genres (item-based)</strong>:<strong> </strong>We will use the movie genres as the only source for recommendations and see how that goes.</p><p>3. <strong>User age+gender (user-based)</strong>: We will use user data as features for our KNN predictor.</p><p>We will create a class that inherits from Surprise’s KNNBasic. Its functionality will be the same as item-based KNN for collaborative filtering, with the only difference being that we will supply a pre-calculated similarity matrix to the fit function, instead of having it calculated from the rating data.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/fccd73cfeca3f7d5b3449d4395fd670c/href">https://medium.com/media/fccd73cfeca3f7d5b3449d4395fd670c/href</a></iframe><p><strong>Part V.I — Content-based filtering using movie plots</strong></p><p>Now, let’s create the TF-IDF similarity matrix</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ebba7c4991de58e2f1899cc4f61131f4/href">https://medium.com/media/ebba7c4991de58e2f1899cc4f61131f4/href</a></iframe><p>Now, get the results for approach #1 (using the complete TF-IDF matrix). Note that we need a different cosine similarity matrix for the regular trainset and the leave-one-out trainset which is used to calculate hit-rate because they contain different inner ids for each movie.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/7dd1796b0061a858225f1ab960193852/href">https://medium.com/media/7dd1796b0061a858225f1ab960193852/href</a></iframe><pre>RMSE: 1.0268<br>HitRate: 0.003973509933774834</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/726/1*jpIh-sFcE_RNjs0ax8Pqtg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/658/1*XklBowYBxwV3VbCWwtwRdQ.png" /></figure><p>Worse than collaborative filtering, without a doubt. We also see that the similarity matrix doesn’t really generate meaningful relationships between movies (the only children’s movie which is similar to Toy Story is Toy Story 2). Let’s see if reducing the number of features helps.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f39f2e89ce2acaf7f0173a22fd2e804d/href">https://medium.com/media/f39f2e89ce2acaf7f0173a22fd2e804d/href</a></iframe><pre>Number of selected features:  784</pre><pre>List of selected features:   [&#39;abbi&#39;, &#39;abel&#39;, &#39;ace&#39;, &#39;adam&#39;, &#39;adel&#39;, &#39;adrian&#39;, &#39;adrien&#39;, &#39;affair&#39;, &#39;agent&#39;, &#39;agn&#39;, &#39;al&#39;, &#39;aladdin&#39;, &#39;alan&#39;, &#39;albert&#39;, &#39;alex&#39;, &#39;alfr&#39;, &#39;ali&#39;, &#39;alic&#39;, &#39;alicia&#39;, &#39;alien&#39;, &#39;allen&#39;, &#39;alli&#39;, &#39;alvin&#39;, &#39;alyssa&#39;, &#39;amanda&#39;, &#39;amelia&#39;, &#39;american&#39;, &#39;ami&#39;, &#39;amo&#39;, &#39;andi&#39;, &#39;andrea&#39;, &#39;andrew&#39;, &#39;angel&#39;, &#39;angela&#39;, &#39;angelo&#39;, &#39;angus&#39;, &#39;ann&#39;, &#39;anna&#39;, &#39;anni&#39;, &#39;antoin&#39;, &#39;anton&#39;, &#39;antonio&#39;, &#39;ape&#39;, &#39;archer&#39;, &#39;archi&#39;, &#39;ariel&#39;, &#39;arjun&#39;, &#39;armstrong&#39;, &#39;arni&#39;, &#39;arroway&#39;, &#39;art&#39;, &#39;arthur&#39;, &#39;arturo&#39;, &#39;ash&#39;, &#39;audrey&#39;, &#39;aurora&#39;, &#39;austin&#39;, &#39;axel&#39;, &#39;babe&#39;, &#39;babi&#39;, &#39;balto&#39;, &#39;bambi&#39;, &#39;band&#39;, &#39;bank&#39;, &#39;barbara&#39;, &#39;barn&#39;, &#39;barney&#39;, &#39;bastian&#39;, &#39;bate&#39;, &#39;bateman&#39;, &#39;batman&#39;, &#39;beach&#39;, &#39;beal&#39;, &#39;bean&#39;, &#39;beatric&#39;, &#39;beckett&#39;, &#39;becki&#39;, &#39;beldar&#39;, &#39;bella&#39;, &#39;ben&#39;, &#39;bendrix&#39;...</pre><pre>RMSE: 1.0314<br>HitRate: 0.003642384105960265</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/719/1*BWj1x-b5fQQFrlqJcHrO5g.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/708/1*oBGvA1nu2OzbNWc5sKArhw.png" /></figure><p>Didn’t really help…We see that a large portion of the informative features are names. We don’t want that because names don’t really say anything about the movie content. Let’s remove the names.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/cc12754ada45305fa33f5857124b14bb/href">https://medium.com/media/cc12754ada45305fa33f5857124b14bb/href</a></iframe><pre>RMSE: 1.0284<br>HitRate: 0.0041390728476821195</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/891/1*NGYrdRAUAZwgIARswe4d-g.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/676/1*VZy1UZht1cB3pXtI4G1r-w.png" /></figure><p>OK, this is somewhat more reasonable than the previous two approaches. First of all the hit-rate is higher (though still way less than the collaborative filtering methods), and second the similar movies to Toy Story(1995) are actually similar. Third, the top predictions for user_1 even seem passable. Let us now try the movie-poster approach and see how that goes.</p><p><strong>Part V.II— Content-based filtering using movie genres</strong></p><p>In this part I will tune down the algorithmic complexity and create a KNN recommender based only on the movie genres. This means that if a person rated children’s movies very high, we should expect the algorithm to recommend children’s movies. We will create all possible combinations of genres using pythons itertools package and then apply our old friend TF-IDF to generate a feature matrix from this data.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ba305925548dfc4bb3c303e6e623a1a2/href">https://medium.com/media/ba305925548dfc4bb3c303e6e623a1a2/href</a></iframe><pre>RMSE: 1.0138<br>HitRate: 0.0031456953642384107</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/885/1*2UzsMJUEYam1xfC1XEKbkw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/535/1*sksGjMCf2SagW71XPB6Jig.png" /></figure><p>Unsurprisingly the most similar movies to Toy Story (1995) are those that have identical or almost identical genres. Other than that the results are quite confusing, why did user_1 get recommendations only for war movies? Let’s remind ourselves of the table in part III of this article (which shows all of the ratings of user_1), we see that there are two movies with genre “war” in user_1’s rating list and both of them got a score of 5. The other genres that user_1 has rated appear more frequently and thus if we average user_1’s scores by genre — the “war” genre will receive a perfect score while the other genres won’t.</p><p>Because the item content-based KNN recommender predicts new items for user_1 by their similarity to other items that have been scored by user_1 it will give a perfect score for each war movie it will see in the context of user_1! The KNN recommender ignores scores belonging to 0 similarity items, it will only take into account the war movie scores when generating the recommendations for war movies and thus always predict perfect scores.</p><p><strong>Part V.III — Content-based filtering using user User age+gender</strong></p><p>Let us start by creating a feature vector from the users’ age and gender. We will assign 0 to ‘Male’ and 1 to ‘Female’ and we will take the age column as-is. Finally we will normalize the columns so as not to be affected by the difference in column magnitudes. Using these values we will create a cosine-similarity matrix as we did for the movies, but this time we will calculate a similarity score between each user. This similarity score will be an input to our user-based KNN recommender.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/eb013182b1375287f9da881a55abac39/href">https://medium.com/media/eb013182b1375287f9da881a55abac39/href</a></iframe><pre>RMSE: 0.9784<br>HitRate: 0.00347682119205298</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/771/1*C3BAeCkUQmfqxy6j5Yn1Ug.png" /></figure><p>We see that the RMSE is a better than for the item-based methods, similarly to the results we got in part IV (collaborative filtering), where the user-based recommendations outperformed item-based ones. but the hit rate is a bit worse than some of the item-based methods we tried. It seems that each method has its strengths and weaknesses and a good approach will probably be to somehow combine them all, and this will be the focus of the next part.</p><h3>Part VI — Conclusions and thoughts ahead</h3><p>In this post, a lot was covered.</p><p>First, we inspected the MovieLens-1M dataset and got some pretty interesting insights from the graphs we saw, such as which movie genres tend to score higher than others in average. We then used some vanilla recommender systems algorithms from the Surprise python package, and got some pretty good results with the SVD algorithm.</p><p>After that, we tried our luck with content-based filtering, but unfortunately, this turned out to be a futile attempt. Despite that, in the research process we unveiled an inherent weakness in KNN recommender systems (or content-based filtering in general), which is that the recommendations for a specific user are based solely on the items that he/she has interacted with in the past, and if there was bias in this user’s interactions it will show up in the KNN recommendations. This is why our spotlight user (user_1) got top scores for war movies when we used genres as the basis of our recommendations, even though we as humans wouldn’t necessarily agree. A more probable recommendation for this user would be children’s/musicals and the like.</p><p>Additionally, we saw that when dealing with natural text, such as movie plots, it is worthwhile to understand which features are most affecting the recommendations. When we removed person names which were ranked very high in the TF-IDF algorithm, we suddenly got better results and this was clearly visible when the movies that were most similar to Toy Story (1995) suddenly made sense.</p><p>In the next part, I will combine content-based filtering and collaborative filtering and thus have the best of both worlds. I already have my eyes on the <a href="https://www.tensorflow.org/recommenders">TensorFlow-recommenders</a> package and I cant’ wait to give it a go.</p><p>Until next time!</p><p>Elad</p><p><strong>References:</strong></p><p>[1]<em> Maxwell Harper and Joseph A. Konstan. 2015. The MovieLens Datasets: History and Context. ACM Transactions on Interactive Intelligent Systems (TiiS) 5, 4: 19:1–19:19. </em><a href="https://doi.org/10.1145/2827872"><em>https://doi.org/10.1145/2827872</em></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8acfeda1ad4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/data-science/movielens-1m-deep-dive-part-i-8acfeda1ad4">MovieLens-1M Deep Dive — Part I</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[Create a simple GUI image processor with PyQt6 and OpenCV]]></title>
            <link>https://towardsdev.com/create-a-simple-gui-image-processor-with-pyqt6-and-opencv-1821e1463691?source=rss-d2d1ff8f0490------2</link>
            <guid isPermaLink="false">https://medium.com/p/1821e1463691</guid>
            <category><![CDATA[pyqt]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[image-processing]]></category>
            <dc:creator><![CDATA[Elad Rapaport]]></dc:creator>
            <pubDate>Sat, 15 Jan 2022 16:41:55 GMT</pubDate>
            <atom:updated>2022-06-10T06:25:30.256Z</atom:updated>
            <content:encoded><![CDATA[<p>Hi all,</p><p>This is a super-short weekend project. We are going to create a simple GUI application in python which highlights specific colors in an image and reports which percentage of the image was highlighted. Let’s show an example of the GUI:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GPVq4azfdApFfTtP40X5EA.png" /><figcaption>Given a source image of an incredible red Volvo, some input regarding the color we want to trace, and the tracing threshold, our program outputs the traced image and reports the percentage of the image which was traced.</figcaption></figure><p>Let’s start our project with some necessary imports:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/46f4b877c07fd23d9e28296a20f56746/href">https://medium.com/media/46f4b877c07fd23d9e28296a20f56746/href</a></iframe><p>Let’s create a MainWindow class which will be the one and only window in our application. We subclass the PyQt class QMainWindow and customize it to suit our needs. The main layout of this application will be a QVBoxLayout which stacks widgets vertically. Inside this layout, we will nest some QHBoxLayouts, which stack widgets horizontally. We also define additional variables which will be of use later in the code.</p><p>Let us continue the code for this class. We define the “select image” and “process buttons” and link their clicked event to some functions we will define later. We also set the RGB selectors, where each of them is a QSpinBox. We must define the maximum and minimum values so a user won’t enter illegal values (only numbers between 0 and 255 are valid RGB values).</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b71c69856ecc3bf2348d9090ad4d8c27/href">https://medium.com/media/b71c69856ecc3bf2348d9090ad4d8c27/href</a></iframe><p>We continue with the MainWindow class. We add the previously created widgets to the top_bar_layout. Then we start creating the widgets in the middle bar — image_bar_layout. This layout contains the source and processed images, including their labels. We also limit the image sizes by using the setMaximumSize methods, so our screen won’t explode with high-res images.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/8cff4a21d7d9b1a8084ed8c32a43405a/href">https://medium.com/media/8cff4a21d7d9b1a8084ed8c32a43405a/href</a></iframe><p>The following code ends the __init__() function of the MainWindow class. We define the bottom_bar_layout which will contain the “Save as file” button to save the processed image. It will also contain a label that will report which percentage of the image has been traced. We will end with the setCentralWidget method that will tell the MainWindow which widget to use. We set a dummy widget called widget whose whole purpose is to contain main_layout, which contains the “meat” of this window.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/45ec4421b685432734f8f3a1b63cceba/href">https://medium.com/media/45ec4421b685432734f8f3a1b63cceba/href</a></iframe><p>The following code contains the remaining methods in the MainWindow class. choose_source_image is used to select an image from the local filesystem. Notice that we resize the image. This is support the processing of larger files by the program (Qt limits image sizes to 128 megabytes).</p><p>process_image does the actual tracing by color. It later resizes the result image and sets the previously defined self.result_image to show the result.</p><p>save_as_file simly utilizes the imwrite function of opencv to save the result in the local file system, given a filename obtained from a filesystem dialog.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/2a8866a9cf1e2a08dab616102f76bcc4/href">https://medium.com/media/2a8866a9cf1e2a08dab616102f76bcc4/href</a></iframe><p>The following are helper functions which 1) resize the image using opencv, 2) create a Qt pixmap from an opencv image, and 3) apply the tracing process, which is the whole purpose of our program.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/7f2600ab964b94192d2989c7fa48ab1c/href">https://medium.com/media/7f2600ab964b94192d2989c7fa48ab1c/href</a></iframe><p>Finally, here is the whole code from start to finish:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6267b87462c983d5be9b6551d7a1749b/href">https://medium.com/media/6267b87462c983d5be9b6551d7a1749b/href</a></iframe><p>As a bonus, to create a python executable from this, all you need to do is pip install pyinstaller and run pyinstaller image_tracer.py from the directory containing the code!</p><p>Thanks all, see you in the next post :)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1821e1463691" width="1" height="1" alt=""><hr><p><a href="https://towardsdev.com/create-a-simple-gui-image-processor-with-pyqt6-and-opencv-1821e1463691">Create a simple GUI image processor with PyQt6 and OpenCV</a> was originally published in <a href="https://towardsdev.com/">Towards Dev</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Foresee the Catastrophe — predicting turbo engine faults using machine learning]]></title>
            <link>https://medium.com/@erap129/foresee-the-catastrophe-predicting-turbo-engine-faults-using-machine-learning-cb8c0344474e?source=rss-d2d1ff8f0490------2</link>
            <guid isPermaLink="false">https://medium.com/p/cb8c0344474e</guid>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[lstm]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[time-series-analysis]]></category>
            <category><![CDATA[deep-learning]]></category>
            <dc:creator><![CDATA[Elad Rapaport]]></dc:creator>
            <pubDate>Thu, 07 Oct 2021 11:03:29 GMT</pubDate>
            <atom:updated>2021-10-07T11:03:29.712Z</atom:updated>
            <content:encoded><![CDATA[<h3>Foresee the Catastrophe — predicting turbo engine faults using machine learning</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Qj84rm9NE-5tryAMBypyUQ.jpeg" /></figure><p>Hello Medium. I have decided to pick up on the super-relevant subject of predictive maintenance and start with the industry gold standard — the NASA Turbofan Engine Degradation Simulation Data Set (<a href="https://ti.arc.nasa.gov/tech/dash/groups/pcoe/prognostic-data-repository/#turbofan">https://ti.arc.nasa.gov/tech/dash/groups/pcoe/prognostic-data-repository/#turbofan</a>).</p><p>My journey was highly influenced by <a href="https://medium.com/u/769bd197e7cf">Koen Peters</a>’s <a href="https://towardsdatascience.com/predictive-maintenance-of-turbofan-engines-ec54a083127">awesome series</a> regarding the analysis of this dataset and his solutions to the problem.</p><p>The dataset is a set of four different datasets: FD001, FD002, FD003, FD004 which are different variations of the same problem. This post will focus on the first dataset: FD001 .</p><p>Each of these datasets consists of numerous multivariate time series of simulated sensor measurements from a fleet of engines. The dataset was used as part of the Prognostics and Health Management (PHM) data competition at PHM’08. The contestants in this challenge were not given any background into the sensor data and were asked to perform pure data-driven predictions, thus we will act accordingly.</p><p>Each time series of sensor measurements starts with a different degree of engine wear and ends at system failure. In other words, we have data from <em>N </em>different scenarios of engine system failures, with <em>N</em> being the number of time individual time series. The test set in each dataset consists of time series with sensor measurements until a certain point in time before system failure which is referred to as remaining useful lifetime (RUL). The goal is to predict the RUL for each time series in the test set as accurately as possible.</p><p>The layout of this post is as follows:</p><ul><li><strong>Part I — Exploratory data analysis (EDA): </strong>In this part, we will perform some basic plotting of the data and understand things such as which sensors contain relevant information and which do not, and we will try to understand the overall behavior of the data concerning the predicted variable.</li><li><strong>Part II — Baseline model: </strong>Here we perform some basic preprocessing and implement a very naive solution to predict the RUL for each time series in the test set. The results for this part will be our baseline score to beat when we try other, more advanced, methods.</li><li><strong>Part III — Feature engineering:</strong> Here we will try two methods of feature engineering. One automatic, using the python package tsfresh , and the other manual, using common sense :)</li><li><strong>Part IV — LSTM</strong>: In this part, we will use an LSTM neural network to process the raw data and predict the RUL for each time series in the test set.</li></ul><h3><strong>Part I — EDA</strong></h3><p>We will start with the necessary imports. These are for the whole project.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/034389fdc7249ccdecba09b3e86c8528/href">https://medium.com/media/034389fdc7249ccdecba09b3e86c8528/href</a></iframe><p>Now we will start by loading the data and viewing a description of each column:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/83c66c4fba9449b246b716f22612527a/href">https://medium.com/media/83c66c4fba9449b246b716f22612527a/href</a></iframe><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/380f2750e8202653c69400a6cea72122/href">https://medium.com/media/380f2750e8202653c69400a6cea72122/href</a></iframe><p>And view the first five rows as well:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6d9bede767bc6a808dd68ede39db3612/href">https://medium.com/media/6d9bede767bc6a808dd68ede39db3612/href</a></iframe><p>We immediately suspect sensors 3, 4, 8, 9, 10, 11, 13, 19, 21, 22 to be non-informative because their standard deviation is very low in relation to their mean value. Sensors 25, 26 contain only NaNs, so we can just drop them.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/5e71ccf173320f1d201cea9721498959/href">https://medium.com/media/5e71ccf173320f1d201cea9721498959/href</a></iframe><p>Usually, having no domain knowledge hinders our ability to extract useful information from the data. Despite this, we will use this to our advantage. Because we don’t know anything about the individual sensors, we can start with normalizing all the data to be on the same scale, this will make our analyses much easier.<br>We will fit a MinMaxScalerto each sensor column, and save these scalers for later use (we will need to perform the same transformation on the test data). After that, we will plot the first unit’s measurements of each signal to verify our assumptions regarding which signals don’t contain relevant information.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ea9c35f3dc10529453a08c7a5c26dd39/href">https://medium.com/media/ea9c35f3dc10529453a08c7a5c26dd39/href</a></iframe><p>The interactive plot below, of the first sensor recording in the dataset, allows us to visually divide the signals into four groups:</p><ul><li>irrelevant 3, 4, 8, 9, 13, 19, 21, 22</li><li>upward trend: 5, 6, 7, 11, 14, 16, 18, 20</li><li>downward trend: 10, 12, 15, 17, 23, 24</li><li>no trend: 1, 2</li></ul><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fdatapane.com%2Fu%2Felad%2Freports%2FBAmpn1k%2Fnasa-fd001%2Fembed%2F&amp;display_name=Datapane&amp;url=https%3A%2F%2Fdatapane.com%2Fu%2Felad%2Freports%2FBAmpn1k%2Fnasa-fd001%2F&amp;image=https%3A%2F%2Fstorage.googleapis.com%2Fdatapane-files-prod%2Fdp%2Fthumbnails%2F78fb5478-c35d-4289-bb99-4c70de367a13.png&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=datapane" width="960" height="700" frameborder="0" scrolling="no"><a href="https://medium.com/media/333685eda22791597e542fedd9a15000/href">https://medium.com/media/333685eda22791597e542fedd9a15000/href</a></iframe><h3>Part II— Baseline Model</h3><p>We will now drop the irrelevant columns, and continue to add labels to the data — the RUL. Our first labeling method will be naive and straightforward. We know that each unit in the training set is run until RUL = 0. Thus, we will assume that the degradation is linear and label each time step in the training set accordingly.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/d3d419a0ccc454238123cc06348afc79/href">https://medium.com/media/d3d419a0ccc454238123cc06348afc79/href</a></iframe><p>Great, now every row has a label. We will now perform regression where each row is a data sample, with x = row sensors and y = RUL. As in Koen Peters&#39; post (<a href="https://towardsdatascience.com/predictive-maintenance-of-turbofan-engines-ec54a083127">https://towardsdatascience.com/predictive-maintenance-of-turbofan-engines-ec54a083127</a>), we clipped the RUL values at a maximum value of 125 to match the engine degradation trend better.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/cec7708f9e84d55c1f68d8b100446cb3/href">https://medium.com/media/cec7708f9e84d55c1f68d8b100446cb3/href</a></iframe><p>Alright, we have our first model. We will use a vanilla XGBoost model throughout the post because of it is generally considered a very good performing model out-of-the-box. Now, let’s load the test set (and not forget to apply the scalers to the test set as well). Below we can see our baseline performance on the train and test sets.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/d2dbdff45acabb88902630ae9c7b91bd/href">https://medium.com/media/d2dbdff45acabb88902630ae9c7b91bd/href</a></iframe><p>Output:</p><pre>RMSE on train set: 17.679585838619715<br>RMSE on test set: 17.35303931484297</pre><p>Alright, so 17.353 is our test RMSE to beat in the following sections.</p><h3>Part III — <strong>Feature engineering</strong></h3><h4>Part IIIa — Windowing the data + a naive regression approach</h4><p>Instead of taking each feature instance to be one timestep of sensor measurements, let’s try using a window of 20 timesteps. The rational behind this is that maybe data from the near past will help determine the RUL of the current timestep.<br>This operation of course will leave us with 3D data (n_samples, timesteps, n_sensors), which we will have to featurize or flatten in order to keep using traditional ML methods.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/2fd82d2fd22461d1e830018eca1f7572/href">https://medium.com/media/2fd82d2fd22461d1e830018eca1f7572/href</a></iframe><p>Our first attempt at this windowed approach will be to flatten the dimensions of each window and simply feed all of it to XGBoost. This will of course hide the time-related information from the model.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0bea32570e4d6734a8c8351ec41801a4/href">https://medium.com/media/0bea32570e4d6734a8c8351ec41801a4/href</a></iframe><p>Output:</p><pre>RMSE on train set: 15.908527707436281<br>RMSE on test set: 16.429387222606096</pre><p>Surprisingly, this approach improves our test RMSE. The XGBoost model was able to make use of the previous 19 samples for each sensor to improve its RUL prediction ability, despite the unstructured nature of the input data (one long array containing measurements from all sensors).</p><h4>Part IIIb — Automatic feature generation</h4><p>Let us now try to generate features from each mini-time series of 20 time steps. We will use the python package tsfresh which generates features automatically from a set of fixed operations.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0ce281ee3298fc62a5e11a0fc8bc2c8a/href">https://medium.com/media/0ce281ee3298fc62a5e11a0fc8bc2c8a/href</a></iframe><p>Output:</p><pre>RMSE on train set: 14.486881328112556<br>RMSE on test set: 23.113512123440437</pre><p>For computational efficiency we used only the features that are not computationally heavy to calculate. For each sensor in each window the following were calculated as features: root_mean_square, mean, median, sum, maximum, minimum, variance and standard_deviation. This resulted in a dataset containing 118 features.</p><p>We can see that the results are extremely overfitted for this method and it proves to be quite unsuccessful compared to the naive windowing approach.</p><h4>Part IIIc— Manual feature generation</h4><p>In this part, we will apply several operations to the measurements of each sensor, in each window. We will avoid creating features that combine sensors because we lack the domain knowledge (for example, the meaning of the sensors) required to generate these kinds of combinations. This approach is quite similar to the automatic feature generation, except the np.difffunction, which computes the difference between each two executive measurements, that was used here and isn’t used in the automatic version.<br>The operations for each sensor will be as follows:</p><ul><li>average value</li><li>average diff value</li><li>standard deviation</li><li>maximum value</li><li>minimum value</li></ul><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/86ecbcc1007c6b177f5e64e0af1b9bdd/href">https://medium.com/media/86ecbcc1007c6b177f5e64e0af1b9bdd/href</a></iframe><p>Output:</p><pre>RMSE on train set: 14.58791927615462<br>RMSE on test set: 18.131375854948576</pre><p>This approach is disappointing as well, with worse test RMSE than the naive windowing approach. Maybe we will have better luck with the LSTM model :)</p><h3>Part III — LSTM</h3><p>In this section we will use a simple LSTM network to process the data windows ans see if we gain any improvement from our baseline model (which the feature approach failed to deliver). We will use Pytorch lightning (the equivalent of Keras for Tensorflow) to streamline our model creation process.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/3378a7dff2e43127c1263106633e436d/href">https://medium.com/media/3378a7dff2e43127c1263106633e436d/href</a></iframe><p>Now let’s create the LSTM model:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/55a0c99c80a5a649ab08f53bf7b8e2fe/href">https://medium.com/media/55a0c99c80a5a649ab08f53bf7b8e2fe/href</a></iframe><p>Now, create the trainer and fit the model with the training data:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/40a0f00074d147ec56515e954a0ab666/href">https://medium.com/media/40a0f00074d147ec56515e954a0ab666/href</a></iframe><p>Finally, get the train and test RMSE for the LSTM approach:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/10369d80c438204e6540b8383f5edd48/href">https://medium.com/media/10369d80c438204e6540b8383f5edd48/href</a></iframe><p>Output:</p><pre>RMSE on train set: 14.761564144403062<br>RMSE on test set: 16.065719776970667</pre><p>We can see that the LSTM approach yielded the best test RMSE for this dataset, and this concludes this article. For future work it is worth investigating the feature generation approach and the reasons for it’s failure. (maybe some domain knowledge will be of use). Moreover, it may be beneficial to experiment with other deep learning architectures and not just LSTM (for example CNNs, transformers).</p><p>Full notebook link: <a href="https://colab.research.google.com/drive/12O670WevZRLm4AvQ_JtyU5cHN5PMnB1C?usp=sharing">https://colab.research.google.com/drive/12O670WevZRLm4AvQ_JtyU5cHN5PMnB1C?usp=sharing</a></p><p>Hope you enjoyed reading!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cb8c0344474e" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>