<?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 Onur-Andros Ozbek on Medium]]></title>
        <description><![CDATA[Stories by Onur-Andros Ozbek on Medium]]></description>
        <link>https://medium.com/@onur_andros_ozbek?source=rss-8d0d34a5e271------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*uVrz21ruadAp7Xu6TDai-A.jpeg</url>
            <title>Stories by Onur-Andros Ozbek on Medium</title>
            <link>https://medium.com/@onur_andros_ozbek?source=rss-8d0d34a5e271------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 17:06:49 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@onur_andros_ozbek/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[Implementing a Deep Q Learning Paper on Colab — Onur Ozbek]]></title>
            <link>https://medium.com/@onur_andros_ozbek/implementing-a-deep-q-learning-paper-on-colab-onur-ozbek-87ebfe565dff?source=rss-8d0d34a5e271------2</link>
            <guid isPermaLink="false">https://medium.com/p/87ebfe565dff</guid>
            <category><![CDATA[deep-q-learning]]></category>
            <category><![CDATA[reinforcement-learning]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[deep-learning]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <dc:creator><![CDATA[Onur-Andros Ozbek]]></dc:creator>
            <pubDate>Tue, 06 Dec 2022 03:04:09 GMT</pubDate>
            <atom:updated>2023-06-08T23:12:41.420Z</atom:updated>
            <content:encoded><![CDATA[<h3>Implementing a Deep Q Learning Paper on Colab — Onur Andros Ozbek</h3><p>In this post, I am going to be implementing the <a href="https://arxiv.org/pdf/1509.02971.pdf">Continuous Control with Deep Reinforcement Learning</a> Paper.</p><p>The paper proposes an actor-critic, model-free algorithm based on the deterministic policy gradient that can operate over continuous action spaces.</p><p>We will go straight into its implementation to avoid a massively large post. I suggest reading the paper in its entirety to gain better intuition.</p><p>First, we install the necessary packages and create some necessary directories:</p><pre>!pip3 install torch opencv-python matplotlib numpy gym swig gym[box2d] box2d box2d-kengz<br>!mkdir ddpg plots # We need these directories.</pre><p>The paper used Ornstein-Uhlenbeck as the noise class, a Replay Buffer, a Critic Network, an Actor Network, and an Agent. The noise class will be used in the Actor class to add extra exploration noise to action selection.</p><pre>class OUActionNoise():<br>    def __init__(self, mu, sigma=0.15, theta=0.2, dt=1e-2, x0=None): # The paper suggests these default values for sigma, theta and dt.<br>        self.theta = theta<br>        self.mu = mu<br>        self.sigma = sigma<br>        self.dt = dt<br>        self.x0 = x0<br>        self.reset() # Resets the temporal correlation<br><br>    def __call__(self):<br>        # This is the equation suggested by the paper.<br>        x = self.x_prev + self.theta * (self.mu - self.x_prev) * self.dt + self.sigma * np.sqrt(self.dt) * np.random.normal(size=self.mu.shape)<br>        self.x_prev = x # Setting x_prev to the current value is how you set the temporal correlations.<br><br>        return x<br>        <br>    # There is no value for x_prev so we do that in the reset function.<br>    def reset(self):<br>        self.x_prev = self.x0 if self.x0 is not None else np.zeros_like(self.mu)</pre><p>This is our Replay Buffer per the paper’s recommendation:</p><pre>class ReplayBuffer():<br>    def __init__(self, max_size, input_shape, n_actions): # We are adding n_actions because we have to save the number of actions in tuples<br>        self.mem_size = max_size<br>        self.mem_cntr = 0 # Keeps a track of the position of the most recently saved memory<br>        self.state_memory = np.zeros((self.mem_size, *input_shape)) # Allocating memory slots with placeholder 0 values<br>        self.new_state_memory = np.zeros((self.mem_size, *input_shape))<br>        self.action_memory = np.zeros((self.mem_size, n_actions))<br>        self.reward_memory = np.zeros(self.mem_size) # Only gets mem_size as shape because its a scalar value<br><br>        # In RL, there is a concept of terminal state. When the episode is over, the agent enters the terminal state<br>        # from which it receives no future rewards so the value of that terminal state is 0.<br>        self.terminal_memory = np.zeros(self.mem_size, dtype=np.bool)<br><br>    def store_transition(self, state, action, reward, state_, done):<br>        index = self.mem_cntr % self.mem_size # Any memory counter less tham memory size just returns the memory counter<br>        self.state_memory[index] = state<br>        self.action_memory[index] = action # Actions are just arrays of arrays<br>        self.reward_memory[index] = reward<br>        self.new_state_memory[index] = state_<br>        self.terminal_memory[index] = done<br><br>        self.mem_cntr += 1 # Increment every time a new memory is stored<br><br>    def sample_buffer(self, batch_size):<br>        max_mem = min(self.mem_cntr, self.mem_size) # The reason why we don&#39;t just use mem_cntr is because mem_cntr goes larger than mem_size<br><br>        batch = np.random.choice(max_mem, batch_size) # Batch is just a random value between 0 to max_mem at the size of batch_size<br><br>        # Gathering the states<br>        states = self.state_memory[batch]<br>        actions = self.action_memory[batch]<br>        rewards = self.reward_memory[batch]<br>        states_ = self.new_state_memory[batch]<br>        dones = self.terminal_memory[batch]<br><br>        return states, actions, rewards, states_, dones</pre><p>The following two classes are the Critic and Actor Networks:</p><pre>class CriticNetwork(nn.Module):<br>    def __init__(self, beta, input_dims, fc1_dims, fc2_dims, n_actions, name, # beta is the learning rate<br>                 chkpt_dir=&#39;/content/ddpg&#39;):<br>        super(CriticNetwork, self).__init__()<br>        self.input_dims = input_dims<br><br>        # Fully connected layer 1 and 2 dimensions<br>        self.fc1_dims = fc1_dims<br>        self.fc2_dims = fc2_dims<br>        self.n_actions = n_actions<br>        self.name = name<br>        self.checkpoint_dir = chkpt_dir<br>        self.checkpoint_file = os.path.join(self.checkpoint_dir, name+&#39;_ddpg&#39;)<br><br>        #### Layers of the Neural Network ####<br>        self.fc1 = nn.Linear(*self.input_dims, self.fc1_dims)<br>        self.fc2 = nn.Linear(self.fc1_dims, self.fc2_dims)<br><br>        self.bn1 = nn.LayerNorm(self.fc1_dims) # To help with convergence<br>        self.bn2 = nn.LayerNorm(self.fc2_dims)<br><br>        self.action_value = nn.Linear(self.n_actions, self.fc2_dims)<br>        <br>        self.q = nn.Linear(self.fc2_dims, 1)<br><br>        f1 = 1./np.sqrt(self.fc1.weight.data.size()[0])<br>        self.fc1.weight.data.uniform_(-f1, f1)<br>        self.fc1.bias.data.uniform_(-f1, f1)<br><br>        f2 = 1./np.sqrt(self.fc2.weight.data.size()[0])<br>        self.fc2.weight.data.uniform_(-f2, f2)<br>        self.fc2.bias.data.uniform_(-f2, f2)<br><br>        f3 = 0.003<br>        self.q.weight.data.uniform_(-f3, f3)<br>        self.q.bias.data.uniform_(-f3, f3)<br><br>        f4 = 1./np.sqrt(self.action_value.weight.data.size()[0])<br>        self.action_value.weight.data.uniform_(-f4, f4)<br>        self.action_value.bias.data.uniform_(-f4, f4)<br><br>        self.optimizer = optim.Adam(self.parameters(), lr=beta, weight_decay=0.01)<br>        self.device = T.device(&#39;cuda:0&#39; if T.cuda.is_available() else &#39;cuda:1&#39;)<br><br>        self.to(self.device)<br><br>    def forward(self, state, action):<br>        state_value = self.fc1(state)<br>        state_value = self.bn1(state_value)<br>        state_value = F.relu(state_value)<br>        state_value = self.fc2(state_value)<br>        state_value = self.bn2(state_value)<br>        action_value = self.action_value(action)<br>        state_action_value = F.relu(T.add(state_value, action_value))<br>        state_action_value = self.q(state_action_value)<br><br>        return state_action_value<br><br>    def save_checkpoint(self):<br>        print(&#39;... saving checkpoint ...&#39;)<br>        T.save(self.state_dict(), self.checkpoint_file) # Creates a state dictionary where the keys are the names of the <br>                                                        # parameters and the vals are the parameters themselves<br><br>    def load_checkpoint(self):<br>        print(&#39;... loading checkpoint ...&#39;)<br>        self.load_state_dict(T.load(self.checkpoint_file))<br><br>    def save_best(self):<br>        print(&#39;... saving best checkpoint ...&#39;)<br>        checkpoint_file = os.path.join(self.checkpoint_dir, self.name+&#39;_best&#39;)<br>        T.save(self.state_dict(), checkpoint_file)<br><br>class ActorNetwork(nn.Module):<br>    def __init__(self, alpha, input_dims, fc1_dims, fc2_dims, n_actions, <br>                 name, # The name is to distinguish between the regular actor network and target actor network and also <br>                       # to allow us to have multiple agents with different names.<br>                 chkpt_dir=&#39;/content/ddpg&#39;):<br>      <br>        super(ActorNetwork, self).__init__()<br>        self.input_dims = input_dims<br>        self.fc1_dims = fc1_dims<br>        self.fc2_dims = fc2_dims<br>        self.n_actions = n_actions # to accomodate bounds where the action is greater than -1<br>        self.name = name<br>        self.checkpoint_dir = chkpt_dir<br>        self.checkpoint_file = os.path.join(self.checkpoint_dir, name+&#39;_ddpg&#39;)<br><br>        self.fc1 = nn.Linear(*self.input_dims, self.fc1_dims)<br>        self.fc2 = nn.Linear(self.fc1_dims, self.fc2_dims)<br><br>        self.bn1 = nn.LayerNorm(self.fc1_dims)<br>        self.bn2 = nn.LayerNorm(self.fc2_dims)<br><br>        self.mu = nn.Linear(self.fc2_dims, self.n_actions)<br><br>        f2 = 1./np.sqrt(self.fc2.weight.data.size()[0])<br>        self.fc2.weight.data.uniform_(-f2, f2)<br>        self.fc2.bias.data.uniform_(-f2, f2)<br><br>        f1 = 1./np.sqrt(self.fc1.weight.data.size()[0])<br>        self.fc1.weight.data.uniform_(-f1, f1)<br>        self.fc1.bias.data.uniform_(-f1, f1)<br><br>        f3 = 0.003<br>        self.mu.weight.data.uniform_(-f3, f3)<br>        self.mu.bias.data.uniform_(-f3, f3)<br><br>        self.optimizer = optim.Adam(self.parameters(), lr=alpha)<br>        self.device = T.device(&#39;cuda:0&#39; if T.cuda.is_available() else &#39;cuda:1&#39;)<br><br>        self.to(self.device)<br><br>    def forward(self, state):<br>        x = self.fc1(state)<br>        x = self.bn1(x)<br>        x = F.relu(x)<br>        x = self.fc2(x)<br>        x = self.bn2(x)<br>        x = F.relu(x)<br>        x = T.tanh(self.mu(x)) # Binds it between -1 and +1<br><br>        return x<br><br>    def save_checkpoint(self):<br>        print(&#39;... saving checkpoint ...&#39;)<br>        T.save(self.state_dict(), self.checkpoint_file)<br><br>    def load_checkpoint(self):<br>        print(&#39;... loading checkpoint ...&#39;)<br>        self.load_state_dict(T.load(self.checkpoint_file))<br><br>    def save_best(self):<br>        print(&#39;... saving best checkpoint ...&#39;)<br>        checkpoint_file = os.path.join(self.checkpoint_dir, self.name+&#39;_best&#39;)<br>        T.save(self.state_dict(), checkpoint_file)</pre><p>Finally, the agent:</p><pre>class Agent():<br>    def __init__(self, alpha, beta, input_dims, tau, n_actions, gamma=0.99,<br>                 max_size=1000000, fc1_dims=400, fc2_dims=300, # Default values from the paper <br>                 batch_size=64):<br>      <br>        self.gamma = gamma<br>        self.tau = tau<br>        self.batch_size = batch_size<br>        self.alpha = alpha<br>        self.beta = beta<br><br>        self.memory = ReplayBuffer(max_size, input_dims, n_actions)<br>        self.noise = OUActionNoise(mu=np.zeros(n_actions))<br>        self.actor = ActorNetwork(alpha, input_dims, fc1_dims, fc2_dims, n_actions=n_actions, name=&#39;actor&#39;)<br>        self.critic = CriticNetwork(beta, input_dims, fc1_dims, fc2_dims, n_actions=n_actions, name=&#39;critic&#39;)<br>        self.target_actor = ActorNetwork(alpha, input_dims, fc1_dims, fc2_dims, n_actions=n_actions, name=&#39;target_actor&#39;)<br>        self.target_critic = CriticNetwork(beta, input_dims, fc1_dims, fc2_dims, n_actions=n_actions, name=&#39;target_critic&#39;)<br><br>        self.update_network_parameters(tau=1) # The purpose of this is to solve the problem of a moving target.<br>                                              # In Q Learning, if you use one network to calculate both the action and the value of that action<br>                                              # then you&#39;re chasing a moving target because you&#39;re updating that estimate every turn. This could lead to<br>                                              # divergence due to using the same parameter. The solution to that is to use a target network that learns the<br>                                              # values of these states and action combinations. Then, the other network is what learns the policy.<br>                                              # You&#39;d also have to periodically overwrite the target networks parameters with the evaluation network network parameters.<br><br>    def choose_action(self, observation):<br>        self.actor.eval() # The actor must go into an evaluation mode.<br>                          # This doesn&#39;t perform an evalutation step. It just tells PyTorch that we don&#39;t want to calculate statistics<br>                          # for the batch normalization. This is critical because without it, the agent will not learn. This step isn&#39;t necessary<br>                          # if we&#39;re not doing batch normalization, which we are (since its critical for convergence). This step also applies when<br>                          # using Dropout.<br><br>        state = T.tensor([observation], dtype=T.float).to(self.actor.device)<br>        mu = self.actor.forward(state).to(self.actor.device)<br>        mu_prime = mu + T.tensor(self.noise(), dtype=T.float).to(self.actor.device)<br>        self.actor.train()<br><br>        return mu_prime.cpu().detach().numpy()[0] # Convert the mu_prime to np because OpenAI gym does not accept tensors<br><br>    # This function acts as an interface for the ReplayBuffer class<br>    def remember(self, state, action, reward, state_, done):<br>        self.memory.store_transition(state, action, reward, state_, done)<br><br>    def save_models(self):<br>        self.actor.save_checkpoint()<br>        self.target_actor.save_checkpoint()<br>        self.critic.save_checkpoint()<br>        self.target_critic.save_checkpoint()<br><br>    def load_models(self):<br>        self.actor.load_checkpoint()<br>        self.target_actor.load_checkpoint()<br>        self.critic.load_checkpoint()<br>        self.target_critic.load_checkpoint()<br><br>    def learn(self):<br>        if self.memory.mem_cntr &lt; self.batch_size: # We don&#39;t want the agent to learn if we haven&#39;t at least filled<br>            return                                 # up the batch size of our memory buffer.<br><br>        states, actions, rewards, states_, done = self.memory.sample_buffer(self.batch_size) # These come back as numpy arrays<br>                                                                                             # So we have to convert them all to tensors (below)<br><br>        states = T.tensor(states, dtype=T.float).to(self.actor.device)<br>        states_ = T.tensor(states_, dtype=T.float).to(self.actor.device)<br>        actions = T.tensor(actions, dtype=T.float).to(self.actor.device)<br>        rewards = T.tensor(rewards, dtype=T.float).to(self.actor.device)<br>        done = T.tensor(done).to(self.actor.device)<br><br>        # Calculating the target actions states like in Q Learning<br>        target_actions = self.target_actor.forward(states_)<br>        critic_value_ = self.target_critic.forward(states_, target_actions)<br>        critic_value = self.critic.forward(states, actions)<br><br>        critic_value_[done] = 0.0<br>        critic_value_ = critic_value_.view(-1)<br><br>        target = rewards + self.gamma*critic_value_<br>        target = target.view(self.batch_size, 1)<br><br>        self.critic.optimizer.zero_grad() # In Pytorch, whenever you calculate the loss function, you want to zero the gradients.<br>                                          # so the gradients from the previous states don&#39;t accumulate and interfere with the calculation.<br>                                          # it can slow stuff down.<br><br>        critic_loss = F.mse_loss(target, critic_value)<br>        critic_loss.backward()<br>        self.critic.optimizer.step()<br><br>        self.actor.optimizer.zero_grad() <br>        actor_loss = -self.critic.forward(states, self.actor.forward(states))<br>        actor_loss = T.mean(actor_loss)<br>        actor_loss.backward()<br>        self.actor.optimizer.step()<br><br>        self.update_network_parameters()<br><br>    def update_network_parameters(self, tau=None): # Tau is a parameter that allows the update of the target network<br>                                                   # to gradually approach the evaluation networks and this is important for a nice slow convergence. You don&#39;t want to<br>                                                   # take too large steps in between updates so tau is a small number less than 1.<br>        if tau is None:<br>            tau = self.tau  # We are doing this because when we call the initializer in the beginning,<br>                            # we want all of the networks to start with the same weight.<br><br>        # Getting all of the names of the parameters from the networks.<br>        actor_params = self.actor.named_parameters()<br>        critic_params = self.critic.named_parameters()<br>        target_actor_params = self.target_actor.named_parameters()<br>        target_critic_params = self.target_critic.named_parameters()<br><br>        # Turning the names to dictionaries for easy iteration.<br>        critic_state_dict = dict(critic_params)<br>        actor_state_dict = dict(actor_params)<br>        target_critic_state_dict = dict(target_critic_params)<br>        target_actor_state_dict = dict(target_actor_params)<br><br>        for name in critic_state_dict:<br>            critic_state_dict[name] = tau*critic_state_dict[name].clone() + \<br>                                (1-tau)*target_critic_state_dict[name].clone()<br><br>        for name in actor_state_dict:<br>             actor_state_dict[name] = tau*actor_state_dict[name].clone() + \<br>                                 (1-tau)*target_actor_state_dict[name].clone()<br><br>        self.target_critic.load_state_dict(critic_state_dict)<br>        self.target_actor.load_state_dict(actor_state_dict)</pre><p>The following function is just a custom plotting function. You can use Matplotlib’s pyplot if you wish.</p><pre>def plot_learning_curve(x, scores, figure_file):<br>    running_avg = np.zeros(len(scores))<br><br>    for i in range(len(running_avg)):<br>        running_avg[i] = np.mean(scores[max(0, i-100):(i+1)])<br><br>    plt.plot(x, running_avg)<br>    plt.title(&#39;Running average of all scores&#39;)<br>    plt.savefig(figure_file)</pre><p>We are basically training the agent against the Lundar Lander simulator by OpenAI. This is the training code:</p><pre>env = gym.make(&#39;LunarLanderContinuous-v2&#39;)<br><br>agent = Agent(alpha=0.0001, <br>              beta=0.001, <br>              input_dims=env.observation_space.shape, <br>              tau=0.001,<br>              batch_size=64, <br>              fc1_dims=400, <br>              fc2_dims=300, <br>              n_actions=env.action_space.shape[0])<br><br>n_games = 1000<br>filename = &#39;LunarLander_alpha_&#39; + str(agent.alpha) + &#39;_beta_&#39; + str(agent.beta) + &#39;_&#39; + str(n_games) + &#39;_games&#39;<br>          <br>figure_file = &#39;plots/&#39; + filename + &#39;.png&#39;<br><br>best_score = env.reward_range[0]<br>score_history = []<br><br>for i in range(n_games):<br>    observation = env.reset()<br>    done = False<br>    score = 0<br>    agent.noise.reset()<br><br>    while not done:<br>        action = agent.choose_action(observation)<br>        observation_, reward, done, info = env.step(action)<br>        agent.remember(observation, action, reward, observation_, done)<br>        agent.learn()<br>        score += reward<br>        observation = observation_<br><br>    score_history.append(score)<br>    avg_score = np.mean(score_history)<br><br>    if avg_score &gt; best_score:<br>        best_score = avg_score<br>        agent.save_models()<br><br>    print(&#39;episode &#39;, i, &#39;score %.1f&#39; % score,&#39;average score %.1f&#39; % avg_score)<br><br>x = [i+1 for i in range(n_games)]<br>plot_learning_curve(x, score_history, figure_file)</pre><p>Here is the performance of the training…</p><pre>episode  0 score -7.6 average score -7.6<br>episode  1 score -1256.8 average score -632.2<br>episode  2 score -521.4 average score -595.3<br>episode  3 score -571.8 average score -589.4<br>episode  4 score -541.8 average score -579.9<br>episode  5 score -1357.5 average score -709.5<br>episode  6 score -794.0 average score -721.5<br>episode  7 score -539.6 average score -698.8<br>episode  8 score -534.7 average score -680.6<br>episode  9 score -807.7 average score -693.3<br>episode  10 score -716.7 average score -695.4<br>episode  11 score -717.5 average score -697.2<br>episode  12 score -736.3 average score -700.3<br>...<br>episode  994 score 287.8 average score 127.1<br>... saving checkpoint ...<br>... saving checkpoint ...<br>... saving checkpoint ...<br>... saving checkpoint ...<br>episode  995 score 274.8 average score 127.2<br>... saving checkpoint ...<br>... saving checkpoint ...<br>... saving checkpoint ...<br>... saving checkpoint ...<br>episode  996 score 249.7 average score 127.3<br>... saving checkpoint ...<br>... saving checkpoint ...<br>... saving checkpoint ...<br>... saving checkpoint ...<br>episode  997 score 253.8 average score 127.5<br>... saving checkpoint ...<br>... saving checkpoint ...<br>... saving checkpoint ...<br>... saving checkpoint ...<br>episode  998 score 266.4 average score 127.6<br>... saving checkpoint ...<br>... saving checkpoint ...<br>... saving checkpoint ...<br>... saving checkpoint ...<br>episode  999 score 279.7 average score 127.7</pre><p>(I’ve truncated the epochs because I trained the agent for 1000 steps).</p><p>Here is the plot:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/383/1*WqSv1RhhEuiy15CQA4FU6A.png" /></figure><p>By: Onur-Andros Ozbek</p><p>Montreal, Quebec</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=87ebfe565dff" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Logo Detection in Livestreams using YOLOv4 — Onur Ozbek]]></title>
            <link>https://medium.com/@onur_andros_ozbek/logo-detection-in-livestreams-using-yolov4-onur-ozbek-8a383393012a?source=rss-8d0d34a5e271------2</link>
            <guid isPermaLink="false">https://medium.com/p/8a383393012a</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[computer-vision]]></category>
            <category><![CDATA[object-detection]]></category>
            <category><![CDATA[python]]></category>
            <dc:creator><![CDATA[Onur-Andros Ozbek]]></dc:creator>
            <pubDate>Mon, 05 Dec 2022 11:08:34 GMT</pubDate>
            <atom:updated>2023-04-18T12:32:04.582Z</atom:updated>
            <content:encoded><![CDATA[<h3>Logo Detection in Livestreams using YOLOv4 — Onur Andros Ozbek</h3><p>This is a project that I worked on during my internship. The goal is to basically detect logos that happen in gaming live streams from content creators, eSports events, etc. Large brands are heavily invested in these streams and they spend a lot of money on sponsorship so it only makes sense that they would want to know if the other side was holding their end of the deal.</p><p>We’ll get right into it. Let’s talk about the data (which was synthetic):</p><p>What I did was two things:</p><ul><li>Get a list of SVG files of logos of interest.</li><li>Get many background images.</li><li>Automatically annotate the logos with bounding boxes (since its an object detection problem)</li></ul><p>The first part doesn’t require a lot of explanation. Let me explain how to go about the 2nd part.</p><p>Using the Twitch API and a Python FFmpeg wrapper, I took N number of Screenshots of M number of Twitch live streams. We need these frames to be able to paste our logos and create a dataset so this part is very important.</p><p>Once this was finished, I wrote a process that would take a random logo file, pass through an augmentation pipeline (rotation, cropping, perspective transformation, etc — based on percentages of what the augmentation is likely to look like in real life), paste on a random image and generate bounding box annotation file automatically in the YOLO format by saving the coordinates</p><p>(Unfortunately, I can’t share any of the code for the above process due to an NDA)</p><p>Once this was done, I went into training and inference on Colab. The following code is mostly provided by Roboflow with minor changes:</p><h4>Configuring cuDNN on Colab for YOLOv4</h4><pre># Change the number depending on what GPU is listed above, under NVIDIA-SMI &gt; Name.<br># Tesla K80: 30<br># Tesla P100: 60<br># Tesla T4: 75\<br>%env compute_capability=60</pre><h4>Installing Darknet for YOLOv4 on Colab</h4><pre>%cd /content/<br>%rm -rf darknet sample_data/<br><br>#we clone the fork of darknet maintained by roboflow<br>#small changes have been made to configure darknet for training<br>!git clone https://github.com/AlexeyAB/darknet.git<br><br>#install environment from the Makefile<br>%cd darknet/<br># compute_30, sm_30 for Tesla K80<br># compute_75, sm_75 for Tesla T4<br># !sed -i &#39;s/ARCH= -gencode arch=compute_60,code=sm_60/ARCH= -gencode arch=compute_30,code=sm_30/g&#39; Makefile<br> <br>#install environment from the Makefile<br>#note if you are on Colab Pro this works on a P100 GPU<br>#if you are on Colab free, you may need to change the Makefile for the K80 GPU<br>#this goes for any GPU, you need to change the Makefile to inform darknet which GPU you are running on.<br>!sed -i &#39;s/OPENCV=0/OPENCV=1/g&#39; Makefile<br> <br>#for GPU accelerated training<br>!sed -i &#39;s/GPU=0/GPU=1/g&#39; Makefile<br>!sed -i &#39;s/CUDNN=0/CUDNN=1/g&#39; Makefile<br>!sed -i &quot;s/ARCH= -gencode arch=compute_75,code=sm_75/ARCH= -gencode arch=compute_${compute_capability},code=sm_${compute_capability}/g&quot; Makefile<br>!make<br><br>%cd /content/darknet<br><br># YOLOv4weights<br>!wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights<br>!wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.conv.137</pre><h4>Set up Custom Dataset for YOLOv4</h4><p>I have uploaded all the training and test images into my Google Drive. The cells below assemble the dataset to be compatible with the YOLO format.</p><pre># Mount Drive to Colab<br>from google.colab import drive<br>drive.mount(&#39;/content/drive&#39;)</pre><pre>%cd /content<br>!mkdir /content/darknet/prediction-images<br>!mkdir /content/darknet/data/obj<br><br>!unzip /content/drive/MyDrive/worlds-lec-dataset.zip -d /content/darknet/logorec/<br>!unzip /content/drive/MyDrive/youtube_images.zip -d /content/darknet/logorec/<br><br>!cp /content/drive/MyDrive/worlds-test-video.mp4 /content/darknet/<br>!cp /content/drive/MyDrive/lec-test-video.mp4 /content/darknet<br><br>%cat /content/darknet/logorec/Worlds-LEC-Dataset/classes.txt | cut -d&quot; &quot; -f2 | cut -d&quot;.&quot; -f1 &gt;&gt; /content/darknet/data/obj.names<br><br>%cd /content/<br>!rm -rf /content/weights</pre><pre># Keeping all imports in a single cell is a good idea so that, <br># if you need to update your imports and add more, you can just<br># update one cell without having to re-run other code.<br><br>import os<br>from random import sample</pre><pre>trainshare = 0.8<br><br>#Set up training file directories for custom dataset<br>%cd /content/darknet/<br><br>#copy image and labels<br>%cp /content/darknet/logorec/Worlds-LEC-Dataset/*.png data/obj/<br>%cp /content/darknet/logorec/Worlds-LEC-Dataset/*.txt data/obj/<br><br>with open(&#39;data/obj.data&#39;, &#39;w&#39;) as out:<br>  out.write(f&#39;classes = 13\n&#39;)<br>  out.write(&#39;train = data/train.txt\n&#39;)<br>  out.write(&#39;valid = data/valid.txt\n&#39;)<br>  out.write(&#39;names = data/obj.names\n&#39;)<br>  out.write(&#39;backup = backup/&#39;)<br> <br>#write train file (just the image list)<br>files = [f for f in os.listdir(&#39;data/obj/&#39;) if f.endswith(&#39;png&#39;)]<br>train = sample(files, int(len(files) * trainshare))<br>validation = set(files) - set(train)<br><br>with open(&#39;data/train.txt&#39;, &#39;w&#39;) as out:<br>  for img in train:<br>    out.write(&#39;data/obj/&#39; + img + &#39;\n&#39;)<br> <br>#write the valid file (just the image list)<br>with open(&#39;data/valid.txt&#39;, &#39;w&#39;) as out:<br>  for img in validation:<br>    out.write(&#39;data/obj/&#39; + img + &#39;\n&#39;)</pre><h4>Write Custom Training Config for YOLOv4</h4><p>We need this config file for YOLOv4. I won’t post the entire thing here as it is way too long.</p><pre>#we build config dynamically based on number of classes<br>#we build iteratively from base config files. This is the same file shape as cfg/yolo-obj.cfg<br>def file_len(fname):<br>  with open(fname) as f:<br>    for i, l in enumerate(f):<br>      pass<br>  return i + 1<br><br>num_classes = file_len(&#39;data/obj.names&#39;)<br>max_batches = num_classes*2000<br>steps1 = .8 * max_batches<br>steps2 = .9 * max_batches<br>steps_str = str(steps1)+&#39;,&#39;+str(steps2)<br>num_filters = (num_classes + 5) * 3<br> <br>print(&quot;writing config for a custom YOLOv4 detector detecting number of classes: &quot; + str(num_classes))<br> <br>#Instructions from the darknet repo<br>#change line max_batches to (classes*2000 but not less than number of training images, and not less than 6000), f.e. max_batches=6000 if you train for 3 classes<br>#change line steps to 80% and 90% of max_batches, f.e. steps=4800,5400<br>if os.path.exists(&#39;./cfg/custom-yolov4-detector.cfg&#39;): <br>  os.remove(&#39;./cfg/custom-yolov4-detector.cfg&#39;)<br> <br>#customize iPython writefile so we can write variables<br>from IPython.core.magic import register_line_cell_magic<br> <br>@register_line_cell_magic<br>def writetemplate(line, cell):<br>    with open(line, &#39;w&#39;) as f:<br>        f.write(cell.format(**globals()))</pre><h4>Train Custom YOLOv4 Detector</h4><p>Training is all about calling the training file.</p><pre>!./darknet detector train data/obj.data cfg/custom-yolov4-detector.cfg yolov4.conv.137 -dont_show -map -clear</pre><h4>Running an inference on test videos and images with Saved YOLOv4 Weights</h4><p>Once it is done, we want to test the model on a video file and save everything in our drive, including the weights.</p><pre>%cd /content<br><br>!zip -r /content/drive/MyDrive/worlds-lec-dataset.zip /content/darknet/logorec/Worlds-LEC-Dataset<br>!zip -r /content/drive/MyDrive/youtube_images.zip /content/darknet/logorec/youtube_images<br><br>!cp /content/darknet/worlds-test-video.mp4 /content/drive/MyDrive/ <br>!cp /content/darknet/lec-test-video.mp4 /content/drive/MyDrive/ </pre><pre>#define utility function<br>def imShow(path):<br>  import cv2<br>  import matplotlib.pyplot as plt<br>  %matplotlib inline<br> <br>  image = cv2.imread(path)<br>  height, width = image.shape[:2]<br>  resized_image = cv2.resize(image,(3*width, 3*height), interpolation = cv2.INTER_CUBIC)<br> <br>  fig = plt.gcf()<br>  fig.set_size_inches(18, 10)<br>  plt.axis(&quot;off&quot;)<br>  plt.imshow(cv2.cvtColor(resized_image, cv2.COLOR_BGR2RGB))<br>  plt.show()</pre><pre><br>#check if weigths have saved yet<br>#backup houses the last weights for our detector<br>#(file yolo-obj_last.weights will be saved to the build\darknet\x64\backup\ for each 100 iterations)<br>#(file yolo-obj_xxxx.weights will be saved to the build\darknet\x64\backup\ for each 1000 iterations)<br>#After training is complete - get result yolo-obj_final.weights from path build\darknet\x64\bac<br>!ls /content/darknet/backup<br>#if it is empty you haven&#39;t trained for long enough yet, you need to train for at least 100 iterations<br><br>#coco.names is hardcoded somewhere in the detector<br>%cd /content/darknet/<br>%cp data/obj.names data/coco.names</pre><h4>The cell below runs inference on every single test image in the test folder.</h4><pre>import random, os, shutil<br><br>#/test has images that we can test our detector on<br>test_folder = &quot;/content/darknet/logorec/youtube_images/&quot;<br>test_images = [f for f in os.listdir(test_folder) if f.endswith(&#39;.png&#39;)]<br>img_path = &#39;&#39;<br>counter = 0<br><br>for i in range(0, len(test_images)):<br>  img_path = test_folder + test_images[i];<br>  !./darknet detect cfg/custom-yolov4-detector.cfg backup/custom-yolov4-detector_best.weights {img_path} -dont-show<br>  imShow(&#39;predictions.jpg&#39;)<br>  src_dir=&#39;/content/darknet/predictions.jpg&#39;<br>  dst_dir=&#39;/content/darknet/prediction-images/prediction-&#39;+str(counter)+&quot;.jpg&quot;<br>  shutil.copy(src_dir,dst_dir)<br>  counter += 1<br><br>%cd /content/darknet/</pre><h4>Saving the weights, the config files, the charts, and the prediction images to the drive</h4><pre>!mkdir /content/darknet/training_results<br><br># Creating the readme file<br>!touch /content/darknet/training_results/readme.txt<br>!echo &quot;The training set was trained on YOLOv4&#39;s default weights with a distractor set&quot; &gt;&gt; /content/darknet/training_results/readme.txt<br><br># Moving the chart to the results directory<br>!cp /content/darknet/chart* /content/darknet/training_results<br><br># Moving the config file to the results directory<br>!cp /content/darknet/cfg/custom-yolov4-detector.cfg /content/darknet/training_results<br><br># Maving the obj.names and obj.data files to the results directory<br>!cp /content/darknet/data/obj.* /content/darknet/training_results<br><br># Moving the weights to the results directory<br>!cp /content/darknet/backup/*.weights /content/darknet/training_results<br><br># Moving the predicted images to the results directory  <br>!cp -r /content/darknet/prediction-images /content/darknet/training_results<br><br># Zip the training result files and copy to the drive<br>!zip -r march13_results.zip /content/darknet/training_results<br>!cp march13_results.zip /content/drive/MyDrive/</pre><pre>!./darknet detector demo data/obj.data cfg/custom-yolov4-tiny-detector.cfg backup/custom-yolov4-tiny-detector_best.weights /content/darknet/worlds-test-video.mp4 -dont_show -thresh 0.25 -out_filename /content/RESULT_worlds-test.mp4<br>!cp /content/RESULT_LEC-test.mp4 /content/drive/MyDrive/<br><br>!./darknet detector demo data/obj.data cfg/custom-yolov4-tiny-detector.cfg backup/custom-yolov4-tiny-detector_best.weights /content/darknet/lec-test-video.mp4 -dont_show -thresh 0.25 -out_filename /content/RESULT_LEC-test.mp4<br>!cp /content/RESULT_worlds-test.mp4 /content/drive/MyDrive/</pre><p>By:</p><p>Onur Andros Ozbek</p><p>Montreal, Quebec</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8a383393012a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Food-101 Classifier using ResNet50 on Colab — Onur Andros Ozbek]]></title>
            <link>https://medium.com/@onur_andros_ozbek/food-101-classifier-using-resnet50-on-colab-f0cc6ac6487?source=rss-8d0d34a5e271------2</link>
            <guid isPermaLink="false">https://medium.com/p/f0cc6ac6487</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[computer-vision]]></category>
            <category><![CDATA[resnet50]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[fastai]]></category>
            <dc:creator><![CDATA[Onur-Andros Ozbek]]></dc:creator>
            <pubDate>Fri, 02 Dec 2022 12:17:05 GMT</pubDate>
            <atom:updated>2023-04-18T12:32:22.470Z</atom:updated>
            <content:encoded><![CDATA[<p>Getting a SOTA accuracy of 88% top-1 on the Food-101 dataset using the ResNet50 model with FastAI on Google Colab.</p><p>Let&#39;s get right into it.</p><p>The shell commands below install the necessary packages. I had to downgrade PyTorch and Torchvision due to compatibility issues with Colab.</p><pre>%reload_ext autoreload<br>%autoreload 2<br><br>!apt-get update<br>!apt install wget<br>!pip install pathlib<br>!pip install fastai<br>!pip install &quot;torch==1.4&quot; &quot;torchvision==0.5.0&quot;</pre><p>The following are my imports. I’ve decided to use the FastAI library. Jeremy Howard had a great lecture on building classifiers. It is a simple library that allows developers to make use of PyTorch and it thought me a lot about multilabel classification.</p><p>Right underneath the imports, I also have the Label Smoothing class. Label smoothing has only been added to FastAI 2.0 beta at the time of this notebook so I had to manually include it.</p><pre>import torch<br>import torch.nn as nn<br>from fastai import *<br>from fastai.vision import *<br>from fastai.callbacks.hooks import *<br>from pathlib import Path<br>import os, shutil<br><br>from torch.distributions.beta import Beta<br><br>def lin_comb(a, b, frac_a): return (frac_a * a) + (1 - frac_a) * b<br><br>def unsqueeze(input, dims):<br>    for dim in listify(dims): input = torch.unsqueeze(input, dim)<br>    return input<br><br>def reduce_loss(loss, reduction=&#39;mean&#39;):<br>    return loss.mean() if reduction==&#39;mean&#39; else loss.sum() if reduction==&#39;sum&#39; else loss<br><br>class LabelSmoothingCrossEntropy(nn.Module):<br>    def __init__(self, ε:float=0.1, reduction=&#39;mean&#39;):<br>        super().__init__()<br>        self.ε,self.reduction = ε,reduction<br><br>    def forward(self, output, target):<br>        c = output.size()[-1]<br>        log_preds = F.log_softmax(output, dim=-1)<br>        loss = reduce_loss(-log_preds.sum(dim=-1), self.reduction)<br>        nll = F.nll_loss(log_preds, target, reduction=self.reduction)<br>        return lin_comb(loss/c, nll, self.ε)</pre><h3>Assembling the Dataset</h3><p>Because I’m using Colab Pro, I have to import my dataset each time I run the notebook. I was able to find the Food-101 dataset on AWS thanks to fastAI because ETH Zurich was no longer hosting the dataset on its website (<a href="https://vision.ee.ethz.ch/datasets_extra/food-101/">https://vision.ee.ethz.ch/datasets_extra/food-101/</a>). The code below downloads the tar file, extracts, renames and reassembles the images based on the data from <strong>train.txt</strong> and <strong>test.txt</strong> files. I prefer to keep all my train images in <strong>food-101/images/train</strong> and all my test images in <strong>food-101/images/test</strong>. I also move all of my meta files to <strong>food-101/meta</strong>.</p><pre># Removing the default sample_data directory that comes with every new Colab instance<br>!rm -rf sample_data<br><br># Removes the existing food-101 dataset for a fresh, complete download<br>!rm -rf food-101 <br><br># Downloads the food-101 dataset tar file<br>!wget https://s3.amazonaws.com/fast-ai-imageclas/food-101.tgz <br><br># Untars the tar file<br>!tar -xvf food-101.tgz <br><br># Removes the redundant h5 directory<br>!rm -rf food-101.tgz food-101/h5/<br><br># Creates the meta, train and test directories inside food-101/images<br>!mkdir food-101/meta food-101/images/train food-101/images/test<br><br># Moves all .txt and .json files to the meta directory<br>!mv food-101/*.txt food-101/*.json food-101/meta/ <br><br># Moves all test images to the food-101/images/test directory and renames them.<br>with open(&#39;food-101/meta/test.txt&#39;) as test_file:<br>  for line in test_file:<br>    name_of_folder = line.split(&#39;/&#39;)[0]<br>    name_of_file = line.split(&#39;/&#39;)[1].rstrip()<br>    Path(&#39;food-101/images/&#39; + name_of_folder + &#39;/&#39; + name_of_file + &#39;.jpg&#39;).rename(&#39;food-101/images/test/&#39; + name_of_folder + &#39;_&#39; + name_of_file + &#39;.jpg&#39;)<br><br># Moves all training images to the food-101/images/train directory and renames them.<br>with open(&#39;food-101/meta/train.txt&#39;) as train_file:<br>  for line in train_file:<br>    name_of_folder = line.split(&#39;/&#39;)[0]<br>    name_of_file = line.split(&#39;/&#39;)[1].rstrip()<br>    Path(&#39;food-101/images/&#39; + name_of_folder + &#39;/&#39; + name_of_file + &#39;.jpg&#39;).rename(&#39;food-101/images/train/&#39; + name_of_folder + &#39;_&#39; + name_of_file + &#39;.jpg&#39;)<br><br># Removes empty directories inside Food-101/images.<br>with open(&#39;food-101/meta/train.txt&#39;) as train_file:<br>  for folder in train_file:<br>    name_of_folder = folder.split(&#39;/&#39;)[0]<br>    if os.path.exists(&#39;food-101/images/&#39; + name_of_folder):<br>      shutil.rmtree(&#39;food-101/images/&#39; + name_of_folder)</pre><p>Running shell commands to see if I’ve partitioned the train and test images correctly. There need to be 75,750 training images and 25,250 test images.</p><pre>%cd /content/food-101/images/train/<br>!ls -1 | wc -l<br><br>%cd ../test<br>!ls -1 | wc -l<br><br>%cd /content/</pre><pre>/content/food-101/images/train<br>75750<br>/content/food-101/images/test<br>25250<br>/content</pre><h3>Building the Model</h3><p>Because every image in the entire dataset comes labeled, I’ve decided to use the names of each image as classes for the classifier via regex. I am training the model with 20% of the training set as my validation set. For data augmentation, I am using fastAI’s .get_transforms() function because the transformations are done on the fly so there isn&#39;t a drastic increase in the dataset size, and the randomness is taken care of by the fastAI library. I am reducing each image to size 224 for better training. The batch size is 8 because I don&#39;t want to exceed the memory limit on CUDA (I&#39;ve had issues with this when I was using larger batch sizes). I&#39;m using .normalize(imagenet_stats) to easily normalize the data based on the RGB channels&#39; stats from the ImageNet dataset.</p><pre>np.random.seed(42)<br>batch_size = 8<br><br>path = &#39;food-101/images/train&#39;<br>file_parse = r&#39;/([^/]+)_\d+\.(png|jpg|jpeg)$&#39;<br><br>data = ImageList.from_folder(path).split_by_rand_pct(valid_pct=0.2).label_from_re(pat=file_parse).transform(get_transforms(), size=224).databunch(bs = batch_size).normalize(imagenet_stats)</pre><p>In my learner where I’ve added ResNet50 as my base architecture, I’m using accuracy for my metric and I’m measuring for top-1. I’m also applying Label smoothing to my model. Label smoothing helps train the model around misclassified data to improve its performance. While it causes the model to learn incorrectly by a lesser percentage, it also decreases the loss. When I relaxed confidence around the labels, I saw that the model was performing much better against the test set.</p><pre>top_1 = partial(top_k_accuracy, k=1)<br>learn = cnn_learner(data, models.resnet50, metrics=[accuracy, top_1], loss_func = LabelSmoothingCrossEntropy(), callback_fns=ShowGraph)</pre><h3>Training</h3><p>This is the training phase of my model. Before I run a set of epochs, I start by finding a learning rate. I added suggestion=True to approximate the interval I&#39;ll be selecting. In the below case, the optimal learning rate was around <strong>1e-06</strong>.</p><pre>learn.lr_find()<br>learn.recorder.plot(suggestion=True)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/648/1*QFDykeKoiaKgUlrOspjCjQ.png" /></figure><p>Based on the suggestion from the learning rate finder, I am getting a slice for the learning rate between <strong>1e-06</strong> and <strong>1e-04</strong>. I’m running 5 epochs on this learning rate and saving the weights as <em>stage-1</em>. The model accuracy progresses in the first run. By the end of the 5th epoch, the model improved from 40.7% accuracy to 58.5%.</p><pre>learn.fit_one_cycle(5, max_lr=slice(1e-06, 1e-04))<br>learn.save(&#39;stage-1&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/409/1*_FzM8G9eStPZ9Kob9-Y7MA.png" /></figure><p>Now I’m finding a new learning rate to run another set of epochs. I’m doing the same thing as above. One thing that I always do is that at the end of each 5 epoch cycle, I unfreeze the model. In this case, my learning rate was roughly around <strong>1e-041</strong>.</p><pre>learn.unfreeze()<br>learn.lr_find()<br>learn.recorder.plot(suggestion=True)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/644/1*MwSTebEh1v3tXZqkpE4z7g.png" /></figure><p>I ran another 5 epochs and saved it as <em>stage-2</em>. The model’s accuracy was increasing as fast as it was in <em>stage-1</em> but this time, it intercepts the validation line.</p><pre>learn.fit_one_cycle(5, max_lr=slice(1e-05, 1e-04))<br>learn.save(&#39;stage-2&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/407/1*FP22o6oDRKRPabOri9CmYg.png" /></figure><p>I wanted to see if I could squeeze out better accuracy. To do this, I recreated the data bunch object. I kept it the same as the earlier declaration except that here, I increased the image size from 224 to 512. I also wanted to load <em>stage 2</em> to run the learning rate finder.</p><pre>data = ImageList.from_folder(path).split_by_rand_pct(valid_pct=0.2).label_from_re(pat=file_parse).transform(get_transforms(), size=512).databunch(bs = batch_size).normalize(imagenet_stats)<br>learn = cnn_learner(data, models.resnet50, metrics=[accuracy, top_1], loss_func = LabelSmoothingCrossEntropy(), callback_fns=ShowGraph)<br>learn.load(&#39;stage-2&#39;)</pre><pre>learn.unfreeze()<br>learn.lr_find()<br>learn.recorder.plot(suggestion=True)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/651/1*PtZwsfP9jjva0ZPbQM_Wxg.png" /></figure><p>I ran another set of 5 epochs on the new learning rate. My last set of epochs gave me an accuracy score of 77.3% but after increasing the image size, I reached 84.2% accuracy. Training time increased significantly (from 11 minutes per epoch to 30 minutes) because I’ve increased the image size.</p><pre>learn.fit_one_cycle(5, max_lr=slice(1e-05, 1e-04))<br>learn.save(&#39;stage-3&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/402/1*soRX4WAIuSXLmnf2rsNJBQ.png" /></figure><p>Although the curve is starting to straighten, I wanted to run an additional set of 5 epochs to get the most accuracy out of my model so I ran a new learning rate finder.</p><pre>learn.unfreeze()<br>learn.lr_find()<br>learn.recorder.plot(suggestion=True)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/646/1*zO4wwCDpeD9qWNhx3v43FQ.png" /></figure><pre>learn.fit_one_cycle(5, max_lr=slice(1e-06, 1e-04))<br>learn.save(&#39;stage-4&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/397/1*-Sa0qLBi38fGlzfwypH-kA.png" /></figure><p>It doesn’t look like I’m going to get better than 84.7% accuracy during training. If I were to run additional epochs, the graph would just oscillate.</p><h3>Mispredictions</h3><p>I wanted to see which images caused the most problems for the model. When it comes to food, there is a lot of visual overlap. For example, it isn’t weird for a model to be confused between steak with gravy and some pudding-like dessert due to the similarities in color, shape, texture, etc.</p><pre>interp = ClassificationInterpretation.from_learner(learn)<br>interp.plot_top_losses(12, figsize=(15, 11))</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/965/1*2ZcZhEDIs7Zd72fAs1BklQ.png" /></figure><pre>interp.most_confused(min_val=5)</pre><pre>[(&#39;filet_mignon&#39;, &#39;steak&#39;, 26),<br> (&#39;chocolate_mousse&#39;, &#39;chocolate_cake&#39;, 24),<br> (&#39;steak&#39;, &#39;filet_mignon&#39;, 22),<br> (&#39;donuts&#39;, &#39;beignets&#39;, 19),<br> (&#39;prime_rib&#39;, &#39;steak&#39;, 19),<br> (&#39;beef_tartare&#39;, &#39;tuna_tartare&#39;, 15),<br> (&#39;dumplings&#39;, &#39;gyoza&#39;, 15),<br> (&#39;chocolate_mousse&#39;, &#39;tiramisu&#39;, 14),<br> (&#39;chocolate_cake&#39;, &#39;chocolate_mousse&#39;, 13),<br> (&#39;pork_chop&#39;, &#39;steak&#39;, 13),<br> (&#39;apple_pie&#39;, &#39;bread_pudding&#39;, 12),<br> (&#39;ice_cream&#39;, &#39;frozen_yogurt&#39;, 12),<br> (&#39;grilled_salmon&#39;, &#39;pork_chop&#39;, 11),<br> (&#39;sushi&#39;, &#39;sashimi&#39;, 11),<br> (&#39;baby_back_ribs&#39;, &#39;steak&#39;, 10),<br> (&#39;chocolate_mousse&#39;, &#39;panna_cotta&#39;, 10),<br> (&#39;ramen&#39;, &#39;pho&#39;, 10),<br> (&#39;tiramisu&#39;, &#39;chocolate_mousse&#39;, 10),<br> (&#39;tuna_tartare&#39;, &#39;beef_tartare&#39;, 10),<br> (&#39;bread_pudding&#39;, &#39;apple_pie&#39;, 9),<br> (&#39;breakfast_burrito&#39;, &#39;huevos_rancheros&#39;, 9),<br> (&#39;lobster_bisque&#39;, &#39;clam_chowder&#39;, 9),<br> (&#39;apple_pie&#39;, &#39;baklava&#39;, 8),<br> (&#39;chicken_quesadilla&#39;, &#39;breakfast_burrito&#39;, 8),<br> (&#39;hamburger&#39;, &#39;pulled_pork_sandwich&#39;, 8),<br> (&#39;tuna_tartare&#39;, &#39;ceviche&#39;, 8),<br> (&#39;baby_back_ribs&#39;, &#39;pork_chop&#39;, 7),<br> (&#39;chocolate_cake&#39;, &#39;tiramisu&#39;, 7),<br> (&#39;falafel&#39;, &#39;tacos&#39;, 7),<br> (&#39;filet_mignon&#39;, &#39;pork_chop&#39;, 7),<br> (&#39;huevos_rancheros&#39;, &#39;nachos&#39;, 7),<br> (&#39;huevos_rancheros&#39;, &#39;tacos&#39;, 7),<br> (&#39;ice_cream&#39;, &#39;chocolate_mousse&#39;, 7),<br> (&#39;pork_chop&#39;, &#39;filet_mignon&#39;, 7),<br> (&#39;pulled_pork_sandwich&#39;, &#39;hamburger&#39;, 7),<br> (&#39;steak&#39;, &#39;prime_rib&#39;, 7),<br> (&#39;beet_salad&#39;, &#39;foie_gras&#39;, 6),<br> (&#39;bruschetta&#39;, &#39;caprese_salad&#39;, 6),<br> (&#39;ceviche&#39;, &#39;tuna_tartare&#39;, 6),<br> (&#39;cheesecake&#39;, &#39;carrot_cake&#39;, 6),<br> (&#39;cheesecake&#39;, &#39;strawberry_shortcake&#39;, 6),<br> (&#39;chicken_quesadilla&#39;, &#39;tacos&#39;, 6),<br> (&#39;escargots&#39;, &#39;french_onion_soup&#39;, 6),<br> (&#39;falafel&#39;, &#39;crab_cakes&#39;, 6),<br> (&#39;foie_gras&#39;, &#39;pork_chop&#39;, 6),<br> (&#39;huevos_rancheros&#39;, &#39;croque_madame&#39;, 6),<br> (&#39;peking_duck&#39;, &#39;spring_rolls&#39;, 6),<br> (&#39;ravioli&#39;, &#39;gnocchi&#39;, 6),<br> (&#39;ravioli&#39;, &#39;lasagna&#39;, 6),<br> (&#39;ravioli&#39;, &#39;shrimp_and_grits&#39;, 6),<br> (&#39;shrimp_and_grits&#39;, &#39;risotto&#39;, 6),<br> (&#39;caprese_salad&#39;, &#39;greek_salad&#39;, 5),<br> (&#39;cheesecake&#39;, &#39;bread_pudding&#39;, 5),<br> (&#39;cheesecake&#39;, &#39;panna_cotta&#39;, 5),<br> (&#39;chicken_curry&#39;, &#39;gnocchi&#39;, 5),<br> (&#39;chicken_quesadilla&#39;, &#39;nachos&#39;, 5),<br> (&#39;club_sandwich&#39;, &#39;grilled_cheese_sandwich&#39;, 5),<br> (&#39;crab_cakes&#39;, &#39;falafel&#39;, 5),<br> (&#39;croque_madame&#39;, &#39;grilled_cheese_sandwich&#39;, 5),<br> (&#39;eggs_benedict&#39;, &#39;croque_madame&#39;, 5),<br> (&#39;french_fries&#39;, &#39;poutine&#39;, 5),<br> (&#39;garlic_bread&#39;, &#39;pizza&#39;, 5),<br> (&#39;greek_salad&#39;, &#39;caesar_salad&#39;, 5),<br> (&#39;grilled_cheese_sandwich&#39;, &#39;club_sandwich&#39;, 5),<br> (&#39;grilled_salmon&#39;, &#39;foie_gras&#39;, 5),<br> (&#39;guacamole&#39;, &#39;nachos&#39;, 5),<br> (&#39;gyoza&#39;, &#39;dumplings&#39;, 5),<br> (&#39;hummus&#39;, &#39;chicken_quesadilla&#39;, 5),<br> (&#39;nachos&#39;, &#39;tacos&#39;, 5),<br> (&#39;onion_rings&#39;, &#39;fried_calamari&#39;, 5),<br> (&#39;pulled_pork_sandwich&#39;, &#39;grilled_cheese_sandwich&#39;, 5),<br> (&#39;risotto&#39;, &#39;fried_rice&#39;, 5),<br> (&#39;risotto&#39;, &#39;shrimp_and_grits&#39;, 5),<br> (&#39;steak&#39;, &#39;pork_chop&#39;, 5),<br> (&#39;strawberry_shortcake&#39;, &#39;cheesecake&#39;, 5)]</pre><p>When I look at the output of .most_confused(), I can see that the model&#39;s confusion was between food that has little to no physical distinction. For example, the biggest confusion was between filet mignon and steak. Filet mignon and steak are actually the same things. Steak is just a broad range of beef cuts and filet mignon is just the cut from the smaller end of the tenderloin.</p><p>It&#39;s usually my habit to include a confusion matrix as well but each time I tried to include one, the kernel would just die and all of my variables would be lost so I decided to go without it.</p><h3>Testing</h3><p>Now it&#39;s time to test the trained model. I created the new data bunch variable data_test. It is the same as above except that in the split_by_folder(), I&#39;ve added the test images.</p><p>I chose learn.validate() as my accuracy method. The reason why is that learn.validate() is used if there is an existing validation set and validation sets are always labeled. The fastAI library takes the test sets blindly even if the test images have labels so I can&#39;t calculate the accuracy on that alone. It made more sense for me to create a new data bunch object where I include my test set like a validation set (split_by_folder(train=&#39;train&#39;, valid=&#39;test&#39;)) and compare my trained model against it.</p><pre>path = &#39;/content/food-101/images&#39;<br><br>data_test = ImageList.from_folder(path).split_by_folder(train=&#39;train&#39;, valid=&#39;test&#39;).label_from_re(file_parse).transform(size=512).databunch().normalize(imagenet_stats)<br>learn = cnn_learner(data, models.resnet50, metrics=[accuracy, top_1],callback_fns=ShowGraph)<br>learn.load(&#39;stage-4&#39;)</pre><p>The validation gives me a SoTA of 88.1% ≈ 88%</p><pre>learn.validate(data_test.valid_dl)</pre><pre>[0.5012631, tensor(0.8809), tensor(0.8809)]</pre><p>By:</p><p>Onur Andros Ozbek</p><p>Montreal, Quebec</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f0cc6ac6487" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>